PoWER#230
Conversation
There was a problem hiding this comment.
Main document describing what PoWER is and how to follow it.
| // 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; |
There was a problem hiding this comment.
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");
}
}| 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(); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Equivalent Rust impl and tests with the same input/output are here: Cuprate/cuprate#568.
| return; | ||
| } | ||
|
|
||
| // Solve PoWER challenge and send to peer. |
There was a problem hiding this comment.
The P2P PoWER flow:
- Node
Ainitiates handshake with nodeB Bsends handshake response which now includes a PoWER challengeAsolves the challenge (indo_handshake_with_peer) and sends the solution toBviaNOTIFY_POWER_SOLUTIONBnow skips PoWER checks when receiving transactions fromA
Worth noting:
- After
Asolves PoWER forB,Awill also enable PoWER forBfor free, i.e. only nodes initiating connections expend compute - The difficulty is always set to
DIFFICULTYalthough technically it's adjustable, seeMAX_DIFFICULTYinpower.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
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| // RPC related code apply to both the RPC and ZMQ-RPC interfaces. | ||
| namespace power |
There was a problem hiding this comment.
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).
src/wallet/wallet2.cpp
Outdated
| // Find PoWER solution if necessary. | ||
| if ((!tools::is_local_address(m_daemon_address)) && (ptx.tx.vin.size() > tools::power::INPUT_THRESHOLD)) |
There was a problem hiding this comment.
I've added PoWER here when sending a TX to a daemon, should other wallet code be changed?
| // 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> |
There was a problem hiding this comment.
https://github.com/tevador/equix/blob/master/LICENSE
https://gitlab.torproject.org/tpo/core/arti/-/blob/main/doc/LGPL-and-rust.md
I'm not sure how this mixes with Monero's current licensing.
since it's a typedef it works now
|
Opened for initial review. FYI I think this will have merge conflicts with #184. edit: resolved conflicts. |

Implements monero-project/research-lab#133.