Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ members = [
"cryptonight",
"helper",
"pruning",
"power",
"test-utils",

# Fuzz
Expand Down Expand Up @@ -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 }
Expand Down
2 changes: 2 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ upper-case-acronyms-aggressive = true
# <https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown>
doc-valid-idents = [
"RandomX",
"PoW",
"PoWER",
# This adds the rest of the default exceptions.
".."
]
24 changes: 24 additions & 0 deletions power/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
127 changes: 127 additions & 0 deletions power/src/challenge.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
/// 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<Self>;

/// 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<PowerSolution> {
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)
}
}
43 changes: 43 additions & 0 deletions power/src/constants.rs
Original file line number Diff line number Diff line change
@@ -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";
Loading
Loading