Skip to content

PoWER#230

Open
hinto-janai wants to merge 29 commits intoseraphis-migration:fcmp++-alpha-stressnetfrom
hinto-janai:power
Open

PoWER#230
hinto-janai wants to merge 29 commits intoseraphis-migration:fcmp++-alpha-stressnetfrom
hinto-janai:power

Conversation

@hinto-janai
Copy link

@hinto-janai hinto-janai commented Nov 11, 2025

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main document describing what PoWER is and how to follow it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main PoWER functions/data.

Comment on lines +71 to +92
// 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 |
inline constexpr uint32_t DIFFICULTY = 100;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

Here's the program that these numbers are from, it tests a few difficulty values using the equivalent Cuprate PoWER impl.

A difficulty somewhere in-between 100~150 seems good if we're targeting 1s of single-threaded compute on consumer hardware.

cargo new power
cd power # replace src/main.rs with the code below 
git clone https://github.com/hinto-janai/cuprate -b power # clone power impl
cargo r -r # run code
//! ```Cargo.toml
//! [package]
//! name = "power"
//! version = "0.1.0"
//! edition = "2024"
//!
//! [dependencies]
//! cuprate-power = { path = "cuprate/power" }
//! rand = "*"
//! ```

fn main() {
    let d = [0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250, 275, 300];
    let iter = 400;

    println!("Testing {iter} iterations for each difficulty: {d:?}");

    let hashes: Vec<([u8; 32], [u8; 32])> = (0..iter)
        .map(|_| (rand::random(), rand::random()))
        .collect();

    let start = std::time::Instant::now();

    for difficulty in d {
        let mut average = 0.0;
        let mut rate = 0.0;

        for (i, (tx_prefix_hash, recent_block_hash)) in hashes.iter().enumerate() {
            cuprate_power::solve_rpc(*tx_prefix_hash, *recent_block_hash, difficulty);

            let i = i as f64;
            let elapsed = start.elapsed().as_secs_f64();
            average = elapsed / i;
            rate = i / elapsed;
        }

        println!("Difficulty: {difficulty}, Average: {average:.4}s, Rate: {rate:.4}/s");
    }
}

Comment on lines +150 to +177
uint32_t create_difficulty_scalar(
const void* challenge,
const size_t challenge_size,
const std::array<uint16_t, 8> solution
) noexcept {
assert(challenge != nullptr);
assert(challenge_size != 0);

blake2b_state state;
blake2b_init(&state, 4);
blake2b_update(&state, PERSONALIZATION_STRING.data(), PERSONALIZATION_STRING.size());
blake2b_update(&state, challenge, challenge_size);
blake2b_update(&state, reinterpret_cast<const uint8_t*>(solution.data()), sizeof(solution));

uint8_t out[4];
blake2b_final(&state, out, 4);

uint32_t scalar;
memcpy_swap32le(&scalar, out, sizeof(scalar));

return scalar;
}

constexpr bool check_difficulty(uint32_t scalar, uint32_t difficulty) noexcept
{
const uint64_t product = uint64_t(scalar) * uint64_t(difficulty);
return product <= std::numeric_limits<uint32_t>::max();
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difficulty functions (and the challenge construction) are derived from Tor's PoW: https://spec.torproject.org/hspow-spec/v1-equix.html, although our functions are slightly different.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Equivalent Rust impl and tests with the same input/output are here: Cuprate/cuprate#568.

return;
}

// Solve PoWER challenge and send to peer.
Copy link
Author

@hinto-janai hinto-janai Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The P2P PoWER flow:

  1. Node A initiates handshake with node B
  2. B sends handshake response which now includes a PoWER challenge
  3. A solves the challenge (in do_handshake_with_peer) and sends the solution to B via NOTIFY_POWER_SOLUTION
  4. B now skips PoWER checks when receiving transactions from A

Worth noting:

  • After A solves PoWER for B, A will also enable PoWER for B for free, i.e. only nodes initiating connections expend compute
  • The difficulty is always set to DIFFICULTY although technically it's adjustable, see MAX_DIFFICULTY in power.h
  • Sending a NOTIFY_POWER_SOLUTION (solving the challenge) is technically optional, although you won't be able to send high-input transactions without it

Comment on lines +1024 to +1040
if (!power_enabled)
{
transaction_prefix tx_prefix;
if (!parse_and_validate_tx_prefix_from_blob(tx, tx_prefix))
{
LOG_PRINT_L1("Incoming transactions failed to parse, rejected");
drop_connection(context, false, false);
return 1;
}

if (tx_prefix.vin.size() >= tools::power::INPUT_THRESHOLD)
{
LOG_PRINT_L1("Incoming transactions failed PoWER, rejected");
drop_connection_with_score(context, tools::power::BAN_SCORE, false);
return 1;
}
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

monero-project/research-lab#133 (comment)

requiring PoW on connection attempts [...] since it would obligate an attacker to expend the same amount of CPU in order to relay a single bad tx (assuming we modify nodes to ban connections that relay txs that fail to verify)

Sending high-input transactions without PoWER eventually leads to a ban here.

Comment on lines +57 to +58
// RPC related code apply to both the RPC and ZMQ-RPC interfaces.
namespace power
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZMQ-RPC re-uses RPC functions, i.e. each send_raw_tx/send_raw_tx_hex call requires PoW, although I think we can skip it? ZMQ-RPC assumes it isn't public (although there is no check or restricted version).

Comment on lines +7776 to +7777
// Find PoWER solution if necessary.
if ((!tools::is_local_address(m_daemon_address)) && (ptx.tx.vin.size() > tools::power::INPUT_THRESHOLD))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added PoWER here when sending a TX to a daemon, should other wallet code be changed?

Comment on lines +1 to +8
// PoWER uses Equi-X:
// <https://github.com/tevador/equix/>.
//
// Equi-X is:
// Copyright (c) 2020 tevador <tevador@gmail.com>
//
// and licensed under the terms of the LGPL version 3.0:
// <https://www.gnu.org/licenses/lgpl-3.0.html>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hinto-janai hinto-janai marked this pull request as ready for review January 29, 2026 23:01
@hinto-janai
Copy link
Author

hinto-janai commented Jan 29, 2026

Opened for initial review. FYI I think this will have merge conflicts with #184.

edit: resolved conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant