diff --git a/Cargo.lock b/Cargo.lock index 300283cec..dfd3103fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -525,6 +525,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 = "blake3" version = "1.8.2" @@ -1459,6 +1468,17 @@ dependencies = [ "tor-rtcompat", ] +[[package]] +name = "cuprate-power" +version = "0.0.1" +dependencies = [ + "blake2", + "equix", + "hex", + "hex-literal 1.0.0", + "pretty_assertions", +] + [[package]] name = "cuprate-pruning" version = "0.1.0" @@ -2077,6 +2097,33 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "dynasm" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a31e49f416ec431ceef002ee220eee9da97687ec3ecea8040703edbaa75e157" +dependencies = [ + "bitflags 2.9.4", + "byteorder", + "lazy_static", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "dynasmrt" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81897269eb88949825a9add5a33fb4456ba6a39811e0909172f21c841457d347" +dependencies = [ + "byteorder", + "dynasm", + "fnv", + "memmap2", +] + [[package]] name = "ecdsa" version = "0.16.9" @@ -2173,6 +2220,19 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "equix" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12603125a0745d93fc87c0c73b8966c11987d534e818b2fdb47cdb41091b01b6" +dependencies = [ + "arrayvec", + "hashx", + "num-traits", + "thiserror 2.0.16", + "visibility", +] + [[package]] name = "errno" version = "0.3.13" @@ -2260,6 +2320,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e178e4fba8a2726903f6ba98a6d221e76f9c12c650d5dc0e6afdc50677b49650" +[[package]] +name = "fixed-capacity-vec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b31a14f5ee08ed1a40e1252b35af18bed062e3f39b69aab34decde36bc43e40" + [[package]] name = "flate2" version = "1.1.2" @@ -2597,6 +2663,21 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "hashx" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b78e3b1b26d755a234b0a2f747fbb4bd961f8c8659de3e392ac56d5057654e12" +dependencies = [ + "arrayvec", + "blake2", + "dynasmrt", + "fixed-capacity-vec", + "hex", + "rand_core 0.9.3", + "thiserror 2.0.16", +] + [[package]] name = "heck" version = "0.5.0" @@ -4131,6 +4212,28 @@ dependencies = [ "toml_edit 0.22.27", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "proc-macro2" version = "1.0.101" diff --git a/Cargo.toml b/Cargo.toml index b3300f808..af3655dca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ members = [ "cryptonight", "helper", "pruning", + "power", "test-utils", # Fuzz @@ -170,6 +171,7 @@ arrayvec = { version = "0.7", default-features = false } arti-client = { version = "0.33", default-features = false } async-trait = { version = "0.1", default-features = false } bitflags = { version = "2", default-features = false } +blake2 = { version = "0.10", default-features = false } blake3 = { version = "1", default-features = false } borsh = { version = "1", default-features = false } bytemuck = { version = "1", default-features = false } diff --git a/clippy.toml b/clippy.toml index d558c4688..ec7cc1939 100644 --- a/clippy.toml +++ b/clippy.toml @@ -4,6 +4,8 @@ upper-case-acronyms-aggressive = true # doc-valid-idents = [ "RandomX", + "PoW", + "PoWER", # This adds the rest of the default exceptions. ".." ] diff --git a/power/Cargo.toml b/power/Cargo.toml new file mode 100644 index 000000000..cf1b7623d --- /dev/null +++ b/power/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "cuprate-power" +version = "0.0.1" +edition = "2024" +description = "PoWER utilities" +license = "MIT" +authors = ["hinto-janai"] +repository = "https://github.com/Cuprate/cuprate/tree/main/cuprate-power" +keywords = ["cuprate", "power"] + +[features] +default = [] + +[dependencies] +equix = { version = "0.5" } +blake2 = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } +hex = { workspace = true } +hex-literal = { workspace = true } + +[lints] +workspace = true diff --git a/power/src/challenge.rs b/power/src/challenge.rs new file mode 100644 index 000000000..2d4607e80 --- /dev/null +++ b/power/src/challenge.rs @@ -0,0 +1,127 @@ +use equix::Solution; + +use crate::{check_difficulty, create_difficulty_scalar}; + +/// Solution to a [`PowerChallenge`]. +/// +/// This struct contains a valid Equi-X challenge and solution that surpasses a difficulty. +pub struct PowerSolution { + /// Equi-X challenge bytes. + pub challenge: Vec, + /// Equi-X solution. + pub solution: Solution, + /// Nonce input that leads to a valid challenge/solution. + pub nonce: u32, +} + +mod sealed { + pub(crate) trait Sealed {} + impl Sealed for crate::PowerChallengeRpc {} + impl Sealed for crate::PowerChallengeP2p {} +} + +#[expect(private_bounds, reason = "Sealed trait")] +/// An Equi-X challenge that must pass a difficulty. +pub trait PowerChallenge +where + Self: sealed::Sealed + + Copy + + Clone + + std::fmt::Debug + + std::hash::Hash + + PartialEq + + Eq + + Ord + + PartialOrd + + AsRef<[u8]>, +{ + /// Typed Equi-X challenge input. + type ChallengeInput; + + /// Byte length of [`Self::ChallengeInput`]. + const SIZE: usize; + + /// Create a new [`PowerChallenge`] using raw challenge bytes (including the nonce). + /// + /// # Errors + /// Returns [`None`] if `challenge` are bytes are malformed. + fn new(challenge: &[u8]) -> Option; + + /// Create a new [`PowerChallenge`] using a typed challenge. + fn new_from_input(input: Self::ChallengeInput) -> Self; + + /// Return the current `nonce` used by the challenge. + fn nonce(&self) -> u32; + + /// Update the `nonce` used by the challenge. + fn update_nonce(&mut self, nonce: u32); + + /// Attempt to solve this [`PowerChallenge`] using the + /// current [`PowerChallenge::nonce`] with the given `difficulty`. + /// + /// # Errors + /// Returns [`None`] if no valid solution was found. + fn try_solve(&self, difficulty: u32) -> Option { + let nonce = self.nonce(); + + let solutions = { + let Ok(t) = equix::solve(self.as_ref()) else { + return None; + }; + + if t.is_empty() { + return None; + } + + t + }; + + for solution in solutions { + let scalar = create_difficulty_scalar(self.as_ref(), &solution); + + if check_difficulty(scalar, difficulty) { + return Some(PowerSolution { + challenge: self.as_ref().to_vec(), + solution, + nonce, + }); + } + } + + None + } + + /// Loop through `nonce` values until a solution is found. + /// + /// This iterates on `1..` assuming that the + /// [`PowerSolution::nonce`] is set to `0`. + /// + /// # Panics + /// This will technically panic if `difficulty` is set to an + /// unrealistically high number which prevents a solution from being found. + /// + /// It should not panic in real use-cases. + fn solve(mut self, difficulty: u32) -> PowerSolution { + for nonce in 1.. { + if let Some(t) = self.try_solve(difficulty) { + return t; + } + + self.update_nonce(nonce); + } + + unreachable!() + } + + /// Verify that `solution`: + /// - is a valid Equi-X solution for this [`PowerChallenge`]. + /// - satisfies `difficulty`. + fn verify(&self, solution: &Solution, difficulty: u32) -> bool { + if equix::verify(self.as_ref(), solution).is_err() { + return false; + } + + let scalar = create_difficulty_scalar(self.as_ref(), solution); + check_difficulty(scalar, difficulty) + } +} diff --git a/power/src/constants.rs b/power/src/constants.rs new file mode 100644 index 000000000..ce5647791 --- /dev/null +++ b/power/src/constants.rs @@ -0,0 +1,43 @@ +/// Ban score for peers that either: +/// - attempt to send high-input transactions without PoWER. +/// - send an invalid or malformed PoWER solution. +pub const BAN_SCORE: usize = 5; + +/// Input counts greater than this require PoWER. +pub const POWER_INPUT_THRESHOLD: usize = 8; + +/// Number of recent block hashes viable for RPC. +pub const POWER_HEIGHT_WINDOW: usize = 2; + +/// Fixed difficulty for the difficulty formula. +/// +/// Target time = ~1s of single-threaded computation. +/// The difficulty value and computation time have a quadratic relationship. +/// Reference values; value of machines are measured in seconds: +/// +/// | Difficulty | Raspberry Pi 5 | Ryzen 5950x | Mac mini M4 | +/// |------------|----------------|-------------|-------------| +/// | 0 | 0.024 | 0.006 | 0.005 | +/// | 25 | 0.307 | 0.076 | 0.067 | +/// | 50 | 0.832 | 0.207 | 0.187 | +/// | 75 | 1.654 | 0.395 | 0.373 | +/// | 100 | 2.811 | 0.657 | 0.611 | +/// | 125 | 4.135 | 0.995 | 0.918 | +/// | 150 | 5.740 | 1.397 | 1.288 | +/// | 175 | 7.740 | 1.868 | 1.682 | +/// | 200 | 9.935 | 2.365 | 2.140 | +/// | 225 | 12.279 | 2.892 | 2.645 | +/// | 250 | 14.855 | 3.573 | 3.226 | +/// | 275 | 17.736 | 4.378 | 3.768 | +/// | 300 | 20.650 | 5.116 | 4.422 | +pub const POWER_DIFFICULTY: u32 = 100; + +/// Max difficulty value. +/// +/// Technically, nodes can be modified to send lower/higher difficulties in P2P. +/// A vanilla node will adjust accordingly; it can and will and solve a higher difficulty challenge. +/// This is the max valid difficulty requested from a peer before the connection is dropped. +pub const POWER_MAX_DIFFICULTY: u32 = POWER_DIFFICULTY * 2; + +/// Personalization string used in PoWER hashes. +pub const POWER_PERSONALIZATION_STRING: &str = "Monero PoWER"; diff --git a/power/src/free.rs b/power/src/free.rs new file mode 100644 index 000000000..3113103c4 --- /dev/null +++ b/power/src/free.rs @@ -0,0 +1,75 @@ +use blake2::{Blake2b, Digest, digest::consts::U4}; +use equix::Solution; + +use crate::{ + POWER_PERSONALIZATION_STRING, PowerChallenge, PowerChallengeP2p, PowerChallengeRpc, + PowerSolution, +}; + +/// Create the difficulty scalar used for [`check_difficulty`]. +pub fn create_difficulty_scalar(challenge: &[u8], solution: &Solution) -> u32 { + let mut h = Blake2b::::new(); + h.update(POWER_PERSONALIZATION_STRING.as_bytes()); + h.update(challenge); + h.update(solution.to_bytes()); + u32::from_le_bytes(h.finalize().into()) +} + +/// Returns [`true`] if `scalar * difficulty <= u32::MAX`. +pub const fn check_difficulty(scalar: u32, difficulty: u32) -> bool { + scalar.checked_mul(difficulty).is_some() +} + +/// Solve a PoWER challenge for RPC. +pub fn solve_rpc( + tx_prefix_hash: [u8; 32], + recent_block_hash: [u8; 32], + difficulty: u32, +) -> PowerSolution { + PowerChallengeRpc::new_from_input((tx_prefix_hash, recent_block_hash, 0)).solve(difficulty) +} + +/// Solve a PoWER challenge for P2P. +pub fn solve_p2p(seed: u64, seed_top64: u64, difficulty: u32) -> PowerSolution { + PowerChallengeP2p::new_from_input((seed, seed_top64, difficulty, 0)).solve(difficulty) +} + +/// Verify a PoWER challenge for RPC. +/// +/// Returns [`true`] if: +/// - `solution` is well-formed. +/// - `solution` satisfies `challenge`. +/// - `solution` passes `difficulty`. +pub fn verify_rpc( + tx_prefix_hash: [u8; 32], + recent_block_hash: [u8; 32], + nonce: u32, + solution: &[u8; 16], + difficulty: u32, +) -> bool { + let Ok(solution) = Solution::try_from_bytes(solution) else { + return false; + }; + PowerChallengeRpc::new_from_input((tx_prefix_hash, recent_block_hash, nonce)) + .verify(&solution, difficulty) +} + +/// Verify a PoWER challenge for P2P. +/// +/// Returns [`true`] if: +/// - `solution` is well-formed. +/// - `solution` satisfies `challenge`. +/// - `solution` passes `difficulty`. +pub fn verify_p2p( + seed: u64, + seed_top64: u64, + difficulty: u32, + nonce: u32, + solution: &[u8; 16], +) -> bool { + let Ok(solution) = Solution::try_from_bytes(solution) else { + return false; + }; + PowerChallengeP2p::new_from_input((seed, seed_top64, difficulty, nonce)) + .verify(&solution, difficulty) +} diff --git a/power/src/lib.rs b/power/src/lib.rs new file mode 100644 index 000000000..5afba2c0f --- /dev/null +++ b/power/src/lib.rs @@ -0,0 +1,59 @@ +//! # `cuprate-power` +//! +//! This crate contains functionality for [PoWER](https://github.com/monero-project/monero/blob/master/docs/POWER.md). +//! +//! # Solutions for wallets/clients +//! +//! Example of wallet/client logic when relaying transactions. +//! +//! ``` +//! use cuprate_power::*; +//! use hex_literal::hex; +//! +//! // If transaction inputs <= `POWER_INPUT_THRESHOLD` +//! // then this can be skipped. +//! +//! let tx_prefix_hash = hex!("a12f6872a2178e5eac25f0eb19cc5b29802d3a53e5eea2004756cbfb0af90590"); +//! let recent_block_hash = hex!("32d50ed6f691416afc78cb4102821b6392f49bae9a3c2edc513f42564379e936"); +//! let nonce = 0; +//! +//! let solution: PowerSolution = solve_rpc( +//! tx_prefix_hash, +//! recent_block_hash, +//! nonce, +//! POWER_DIFFICULTY +//! ); +//! +//! // Now include: +//! // +//! // - `tx_prefix_hash` +//! // - `recent_block_hash` +//! // - `solution.solution` +//! // - `solution.nonce` +//! // +//! // when sending a transaction via daemon RPC. +//! ``` +//! +//! TODO: LGPL-3 +//! +//! + +#[cfg(test)] +use hex_literal as _; + +#[cfg(test)] +mod tests; + +mod challenge; +mod constants; +mod free; +mod p2p; +mod rpc; + +pub use equix; + +pub use challenge::{PowerChallenge, PowerSolution}; +pub use constants::*; +pub use free::*; +pub use p2p::PowerChallengeP2p; +pub use rpc::PowerChallengeRpc; diff --git a/power/src/p2p.rs b/power/src/p2p.rs new file mode 100644 index 000000000..3145cf69e --- /dev/null +++ b/power/src/p2p.rs @@ -0,0 +1,74 @@ +use crate::{POWER_PERSONALIZATION_STRING, PowerChallenge}; + +const SIZE: usize = POWER_PERSONALIZATION_STRING.len() + + size_of::() + + size_of::() + + size_of::() + + size_of::(); + +const _: () = assert!(SIZE == 36); + +#[repr(transparent)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Ord, PartialOrd)] +/// A [`PowerChallenge`] for the P2P interface. +/// +/// This creates well-formed challenges for P2P. +pub struct PowerChallengeP2p([u8; SIZE]); + +impl AsRef<[u8]> for PowerChallengeP2p { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl From<[u8; SIZE]> for PowerChallengeP2p { + fn from(t: [u8; SIZE]) -> Self { + Self(t) + } +} + +impl From for [u8; SIZE] { + fn from(t: PowerChallengeP2p) -> Self { + t.0 + } +} + +impl PowerChallenge for PowerChallengeP2p { + /// `(seed || seed_top64 || difficulty || nonce)` + type ChallengeInput = (u64, u64, u32, u32); + + const SIZE: usize = SIZE; + + fn new(challenge: &[u8]) -> Option { + match challenge.try_into() { + Ok(t) => Some(Self(t)), + Err(_) => None, + } + } + + fn new_from_input(input: Self::ChallengeInput) -> Self { + let seed = { + let (low, high) = (input.0, input.1); + (u128::from(high) << 64) | u128::from(low) + }; + let difficulty = input.2; + let nonce = input.3; + + let mut this = [0; SIZE]; + + this[..12].copy_from_slice(POWER_PERSONALIZATION_STRING.as_bytes()); + this[12..28].copy_from_slice(&u128::to_le_bytes(seed)); + this[28..32].copy_from_slice(&u32::to_le_bytes(difficulty)); + this[32..36].copy_from_slice(&u32::to_le_bytes(nonce)); + + Self(this) + } + + fn update_nonce(&mut self, nonce: u32) { + self.0[32..].copy_from_slice(&u32::to_le_bytes(nonce)); + } + + fn nonce(&self) -> u32 { + u32::from_le_bytes(self.0[32..36].try_into().unwrap()) + } +} diff --git a/power/src/rpc.rs b/power/src/rpc.rs new file mode 100644 index 000000000..6ac58d303 --- /dev/null +++ b/power/src/rpc.rs @@ -0,0 +1,69 @@ +use crate::{POWER_PERSONALIZATION_STRING, PowerChallenge}; + +const SIZE: usize = POWER_PERSONALIZATION_STRING.len() + + size_of::<[u8; 32]>() + + size_of::<[u8; 32]>() + + size_of::(); + +const _: () = assert!(SIZE == 80); + +#[repr(transparent)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Ord, PartialOrd)] +/// A [`PowerChallenge`] for the RPC interface. +/// +/// This creates well-formed challenges for RPC. +pub struct PowerChallengeRpc([u8; SIZE]); + +impl AsRef<[u8]> for PowerChallengeRpc { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl From<[u8; SIZE]> for PowerChallengeRpc { + fn from(t: [u8; SIZE]) -> Self { + Self(t) + } +} + +impl From for [u8; SIZE] { + fn from(t: PowerChallengeRpc) -> Self { + t.0 + } +} + +impl PowerChallenge for PowerChallengeRpc { + /// `(tx_prefix_hash || recent_block_hash || nonce)` + type ChallengeInput = ([u8; 32], [u8; 32], u32); + + const SIZE: usize = SIZE; + + fn new(challenge: &[u8]) -> Option { + match challenge.try_into() { + Ok(t) => Some(Self(t)), + Err(_) => None, + } + } + + fn new_from_input(input: Self::ChallengeInput) -> Self { + let tx_prefix_hash = input.0; + let recent_block_hash = input.1; + let nonce = input.2; + + let mut this = [0; SIZE]; + this[..12].copy_from_slice(POWER_PERSONALIZATION_STRING.as_bytes()); + this[12..44].copy_from_slice(&tx_prefix_hash); + this[44..76].copy_from_slice(&recent_block_hash); + this[76..].copy_from_slice(&u32::to_le_bytes(nonce)); + + Self(this) + } + + fn update_nonce(&mut self, nonce: u32) { + self.0[76..].copy_from_slice(&u32::to_le_bytes(nonce)); + } + + fn nonce(&self) -> u32 { + u32::from_le_bytes(self.0[76..80].try_into().unwrap()) + } +} diff --git a/power/src/tests.rs b/power/src/tests.rs new file mode 100644 index 000000000..fcedf54f3 --- /dev/null +++ b/power/src/tests.rs @@ -0,0 +1,229 @@ +use pretty_assertions::assert_eq; + +use crate::{ + PowerChallenge, PowerChallengeP2p, PowerChallengeRpc, check_difficulty, + create_difficulty_scalar, verify_p2p, verify_rpc, +}; + +/// Test difficulty, real difficulty value is too high for debug builds. +const DIFF: u32 = 15; + +struct TestDataEquix { + challenge: &'static str, + expected_solution: &'static str, + expected_solution_count: usize, + expected_scalar: u32, +} + +#[derive(Debug)] +struct TestDataRpc { + tx_prefix_hash: &'static str, + recent_block_hash: &'static str, + expected_nonce: u32, + /// Bytes of [`PowerSolution::challenge`], + /// _not_ the initial challenge construction. + expected_challenge: &'static str, + expected_solution: &'static str, + expected_scalar: u32, +} + +struct TestDataP2p { + seed: u64, + seed_top64: u64, + expected_nonce: u32, + /// Bytes of [`PowerSolution::challenge`], + /// _not_ the initial challenge construction. + expected_challenge: &'static str, + expected_solution: &'static str, + expected_scalar: u32, +} + +const TEST_DATA_EQUIX: [TestDataEquix; 5] = [ + // test UTF8 + TestDataEquix { + challenge: "よ、ひさしぶりだね。", + expected_solution: "546658a95f6466ecc41b24dca5a5e8f5", + expected_solution_count: 3, + expected_scalar: 609012647, + }, + TestDataEquix { + challenge: "👋,🕒👉🕘.", + expected_solution: "7854ba6c1c9bf7cc9354aed876ce64f4", + expected_solution_count: 3, + expected_scalar: 1651207227, + }, + TestDataEquix { + challenge: "Privacy is necessary for an open society in the electronic age.", + expected_solution: "7d1467364825e586ae44b9e95ff388f3", + expected_solution_count: 4, + expected_scalar: 2074493700, + }, + TestDataEquix { + challenge: "We must defend our own privacy if we expect to have any.", + expected_solution: "a330e6561142a57be57513c1095d46ff", + expected_solution_count: 3, + expected_scalar: 1892198895, + }, + TestDataEquix { + challenge: "We must come together and create systems which allow anonymous transactions to take place.", + expected_solution: "ca1e0362d9252bbb85c62fcdf4ac68f6", + expected_solution_count: 2, + expected_scalar: 283799637, + }, +]; + +const TEST_DATA_RPC: [TestDataRpc; 3] = [ + TestDataRpc { + tx_prefix_hash: "c01d4920b75c0cad3a75aa71d6aa73e3d90d0be3ac8da5f562b3fc101e74b57c", + recent_block_hash: "77ff034133bdd86914c6e177563ee8b08af896dd2603b882e280762deab609c0", + expected_nonce: 5, + expected_challenge: "4d6f6e65726f20506f574552c01d4920b75c0cad3a75aa71d6aa73e3d90d0be3ac8da5f562b3fc101e74b57c77ff034133bdd86914c6e177563ee8b08af896dd2603b882e280762deab609c005000000", + expected_solution: "6c81ba867f822ea88b14fe2ed027e1ee", + expected_scalar: 259977672, + }, + TestDataRpc { + tx_prefix_hash: "17bac54d909964de0ed46eda755904b33fb42eead7ce015fbdde17fa6f0ec95f", + recent_block_hash: "6d4c090582ed8cecfc8f8d90ddd8e6b7c8b39dd86c7e882078b670a7ba29b03f", + expected_nonce: 24, + expected_challenge: "4d6f6e65726f20506f57455217bac54d909964de0ed46eda755904b33fb42eead7ce015fbdde17fa6f0ec95f6d4c090582ed8cecfc8f8d90ddd8e6b7c8b39dd86c7e882078b670a7ba29b03f18000000", + expected_solution: "6992d7cb29ae95dbc92f6b8d50e820ef", + expected_scalar: 252939049, + }, + TestDataRpc { + tx_prefix_hash: "6dd6a8df16e052f53d51f5f76372ab0c14c60d748908c4589a90327bdc6498a1", + recent_block_hash: "bc322459b35f5c58082d4193c8d6bf4f057aedd0823121f2ecbcb117276d13a2", + expected_nonce: 1, + expected_challenge: "4d6f6e65726f20506f5745526dd6a8df16e052f53d51f5f76372ab0c14c60d748908c4589a90327bdc6498a1bc322459b35f5c58082d4193c8d6bf4f057aedd0823121f2ecbcb117276d13a201000000", + expected_solution: "19018e8d20beaeda149816cd74f33bfd", + expected_scalar: 187745649, + }, +]; + +const TEST_DATA_P2P: [TestDataP2p; 3] = [ + TestDataP2p { + seed: 0, + seed_top64: 0, + expected_nonce: 10, + expected_challenge: "4d6f6e65726f20506f574552000000000000000000000000000000000f0000000a000000", + expected_solution: "ad025bac4c7bb2dfcb4bb666cf2643e8", + expected_scalar: 252557470, + }, + TestDataP2p { + seed: 1_589_356, + seed_top64: 6700, + expected_nonce: 0, + expected_challenge: "4d6f6e65726f20506f5745526c401800000000002c1a0000000000000f00000000000000", + expected_solution: "0d25ad67fb065baae91a0d29a31db9d8", + expected_scalar: 50548387, + }, + TestDataP2p { + seed: u64::MAX, + seed_top64: u64::MAX, + expected_nonce: 4, + expected_challenge: "4d6f6e65726f20506f574552ffffffffffffffffffffffffffffffff0f00000004000000", + expected_solution: "3357a279712c70e3e26442d864282ef8", + expected_scalar: 170469575, + }, +]; + +/// Sanity test Equi-X. +#[test] +fn equix() { + for t in TEST_DATA_EQUIX { + let s = equix::solve(t.challenge.as_bytes()).unwrap(); + let solution_count = s.len(); + let solution = s.first().unwrap(); + + assert_eq!(t.expected_solution_count, solution_count); + assert_eq!(t.expected_solution, hex::encode(solution.to_bytes())); + + let scalar = create_difficulty_scalar(t.challenge.as_bytes(), solution); + assert_eq!(t.expected_scalar, scalar); + } +} + +#[test] +fn rpc() { + for t in TEST_DATA_RPC { + println!("{t:?}"); + + let tx_prefix_hash = hex::decode(t.tx_prefix_hash).unwrap().try_into().unwrap(); + let recent_block_hash = hex::decode(t.recent_block_hash) + .unwrap() + .try_into() + .unwrap(); + + let c1 = PowerChallengeRpc::new_from_input((tx_prefix_hash, recent_block_hash, 0)); + let c2 = c1.as_ref(); + drop(equix::solve(c2).unwrap()); + + let s = c1.solve(DIFF); + + assert_eq!(hex::encode(&s.challenge), t.expected_challenge); + + let h = hex::encode(s.solution.to_bytes()); + assert_eq!(h, t.expected_solution); + + assert_eq!(s.nonce, t.expected_nonce); + + let c3 = PowerChallengeRpc::new_from_input(( + tx_prefix_hash, + recent_block_hash, + t.expected_nonce, + )); + assert_eq!(hex::encode(c3.as_ref()), t.expected_challenge); + + let d = create_difficulty_scalar(&s.challenge, &s.solution); + assert_eq!(d, t.expected_scalar); + + let last_difficulty_that_passes = u32::MAX / d; + + assert_eq!(true, check_difficulty(d, last_difficulty_that_passes)); + assert_eq!(false, check_difficulty(d, last_difficulty_that_passes + 1)); + + assert!(verify_rpc( + tx_prefix_hash, + recent_block_hash, + t.expected_nonce, + &s.solution.to_bytes(), + DIFF, + )); + } +} + +#[test] +fn p2p() { + for t in TEST_DATA_P2P { + let c1 = PowerChallengeP2p::new_from_input((t.seed, t.seed_top64, DIFF, 0)); + let c2 = c1.as_ref(); + drop(equix::solve(c2).unwrap()); + + let s = c1.solve(DIFF); + + assert_eq!(hex::encode(&s.challenge), t.expected_challenge); + + let h = hex::encode(s.solution.to_bytes()); + assert_eq!(h, t.expected_solution); + + assert_eq!(s.nonce, t.expected_nonce); + + let c3 = PowerChallengeP2p::new_from_input((t.seed, t.seed_top64, DIFF, t.expected_nonce)); + assert_eq!(hex::encode(c3.as_ref()), t.expected_challenge); + + let d = create_difficulty_scalar(&s.challenge, &s.solution); + assert_eq!(d, t.expected_scalar); + + let last_difficulty_that_passes = u32::MAX / d; + + assert_eq!(true, check_difficulty(d, last_difficulty_that_passes)); + assert_eq!(false, check_difficulty(d, last_difficulty_that_passes + 1)); + + assert!(verify_p2p( + t.seed, + t.seed_top64, + DIFF, + t.expected_nonce, + &s.solution.to_bytes(), + )); + } +}