Skip to content
Open
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
44 changes: 44 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ axum = "0.7"
base64 = "0.22"
bs58 = "0.5"
chrono = "0.4"
curve25519-dalek = "4.1"
dotenvy = "0.15"
flume = "0.11"
hex = "0.4"
Expand Down
1 change: 1 addition & 0 deletions crates/pool-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ anyhow.workspace = true
argon2.workspace = true
bs58.workspace = true
chrono.workspace = true
curve25519-dalek.workspace = true
hex.workspace = true
serde.workspace = true
serde_json.workspace = true
Expand Down
55 changes: 51 additions & 4 deletions crates/pool-common/src/protocol.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use curve25519_dalek::ristretto::CompressedRistretto;
use serde::{Deserialize, Serialize};
use sha3::{Digest, Sha3_256};

Expand Down Expand Up @@ -137,6 +138,7 @@ fn parse_address_network(address: &str) -> Result<Option<AddressNetwork>, String
None
};
if let Some(network) = network {
validate_stealth_public_keys(payload)?;
Ok(Some(network))
} else {
Err("invalid address checksum".to_string())
Expand All @@ -148,6 +150,30 @@ fn parse_address_network(address: &str) -> Result<Option<AddressNetwork>, String
}
}

fn validate_stealth_public_keys(payload: &[u8]) -> Result<(), String> {
if payload.len() != 64 {
return Err("invalid address length".to_string());
}

let spend_ok = CompressedRistretto::from_slice(&payload[..32])
.map_err(|_| "invalid address spend public key".to_string())?
.decompress()
.is_some();
if !spend_ok {
return Err("invalid address spend public key".to_string());
}

let view_ok = CompressedRistretto::from_slice(&payload[32..64])
.map_err(|_| "invalid address view public key".to_string())?
.decompress()
.is_some();
if !view_ok {
return Err("invalid address view public key".to_string());
}

Ok(())
}

pub fn parse_hash_hex(v: &str) -> Result<[u8; 32], String> {
let trimmed = v.trim();
let raw = hex::decode(trimmed).map_err(|err| match err {
Expand Down Expand Up @@ -181,9 +207,23 @@ fn address_checksum(payload: &[u8], network_id: &str) -> [u8; 32] {
#[cfg(test)]
mod tests {
use super::*;
use curve25519_dalek::constants::RISTRETTO_BASEPOINT_TABLE;
use curve25519_dalek::scalar::Scalar;

fn test_address_payload(seed: u8) -> [u8; 64] {
let spend_scalar = Scalar::from_bytes_mod_order([seed.max(1); 32]);
let view_scalar = Scalar::from_bytes_mod_order([seed.wrapping_add(1).max(1); 32]);
let spend_pub = (&spend_scalar * RISTRETTO_BASEPOINT_TABLE).compress();
let view_pub = (&view_scalar * RISTRETTO_BASEPOINT_TABLE).compress();

let mut payload = [0u8; 64];
payload[..32].copy_from_slice(spend_pub.as_bytes());
payload[32..].copy_from_slice(view_pub.as_bytes());
payload
}

fn test_miner_address(seed: u8) -> String {
let payload = [seed; 64];
let payload = test_address_payload(seed);
let mut encoded = payload.to_vec();
encoded.extend_from_slice(&address_checksum(&payload, NETWORK_ID_MAINNET)[..4]);
bs58::encode(encoded).into_string()
Expand Down Expand Up @@ -239,7 +279,7 @@ mod tests {
Some(AddressNetwork::Mainnet)
);

let payload = [0x22; 64];
let payload = test_address_payload(0x23);
let mut testnet = payload.to_vec();
testnet.extend_from_slice(&address_checksum(&payload, NETWORK_ID_TESTNET)[..4]);
let testnet_addr = bs58::encode(testnet).into_string();
Expand All @@ -252,7 +292,7 @@ mod tests {

#[test]
fn network_specific_validation_rejects_cross_network_checksum() {
let payload = [0x55; 64];
let payload = test_address_payload(0x55);
let mut encoded = payload.to_vec();
encoded.extend_from_slice(&address_checksum(&payload, NETWORK_ID_TESTNET)[..4]);
let address = bs58::encode(encoded).into_string();
Expand All @@ -277,7 +317,7 @@ mod tests {

#[test]
fn miner_address_rejects_bad_checksum() {
let payload = [0x44; 64];
let payload = test_address_payload(0x44);
let mut encoded = payload.to_vec();
encoded.extend_from_slice(&address_checksum(&payload, NETWORK_ID_MAINNET)[..4]);
let last = encoded
Expand All @@ -288,4 +328,11 @@ mod tests {
let err = validate_miner_address(&address).expect_err("must reject");
assert!(err.contains("checksum"));
}

#[test]
fn miner_address_rejects_checksummed_invalid_ristretto_keys() {
let address = "S7YPHt98NDKrUNmFaHa9GQu4XJvRPkTR51bxdE4122UFxfB4cqdFP5R2pkJSrNTQGwmFVmKzKodu7F8XmHjTTx9PNx3i";
let err = validate_miner_address(address).expect_err("must reject");
assert!(err.contains("public key"));
}
}
2 changes: 2 additions & 0 deletions crates/pool-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ pool-common = { path = "../pool-common" }

[dev-dependencies]
bs58.workspace = true
curve25519-dalek.workspace = true
sha3.workspace = true
tempfile.workspace = true
5 changes: 3 additions & 2 deletions crates/pool-runtime/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2237,6 +2237,7 @@ impl NodeApi for InMemoryNode {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::test_miner_address as generated_test_miner_address;
use pool_common::pow::{difficulty_to_target, DeterministicTestHasher, PowHasher};

fn cfg() -> Config {
Expand Down Expand Up @@ -2269,11 +2270,11 @@ mod tests {
}

fn test_miner_address() -> String {
"3EWAEECjhATNX9CHB9ZUSN6jT9FkhhUF22mQruRtAroUpsvegu5XVeJub2t5hRqufQjkc4QNQcPK1cTnco3DdrvWuEX3W".to_string()
generated_test_miner_address(0x31)
}

fn other_miner_address() -> String {
"5U1KTTQUPKuk3HPZMJ7wsjCTuHWWQPwV34XpiormLibxekrJNoA3zHcpB4mAPrgpKpUWD7ojpDmd2DvaEb5SGir81hpWR".to_string()
generated_test_miner_address(0x32)
}

#[test]
Expand Down
3 changes: 3 additions & 0 deletions crates/pool-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ pub mod stats;

pub mod store;

#[cfg(test)]
pub(crate) mod test_support;

pub(crate) mod stratum;

pub mod telemetry;
Expand Down
5 changes: 2 additions & 3 deletions crates/pool-runtime/src/payout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3231,6 +3231,7 @@ mod tests {
use super::*;
use crate::node::HttpError;
use crate::store::PoolStore;
use crate::test_support::test_miner_address;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine as _;
use pool_common::db::{DbShare, ShareReplayData};
Expand Down Expand Up @@ -3386,9 +3387,7 @@ mod tests {
}

fn test_address(seed: u8) -> String {
let mut payload = [seed.max(1); 64];
payload[0] = seed.max(1);
bs58::encode(payload).into_string()
test_miner_address(seed)
}

fn wallet_output(txid: &str, output_index: u32, amount: u64) -> WalletOutput {
Expand Down
3 changes: 2 additions & 1 deletion crates/pool-runtime/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,7 @@ async fn resolve_expected_address_network(
mod tests {
use super::validate_pool_fee_destination_config;
use crate::config::Config;
use crate::test_support::test_miner_address;

#[test]
fn pool_fee_destination_is_optional_when_fee_is_disabled() {
Expand Down Expand Up @@ -630,7 +631,7 @@ mod tests {
fn valid_pool_fee_destination_is_accepted() {
let cfg = Config {
pool_fee_pct: 1.0,
pool_fee_wallet_address: "3EWAEECjhATNX9CHB9ZUSN6jT9FkhhUF22mQruRtAroUpsvegu5XVeJub2t5hRqufQjkc4QNQcPK1cTnco3DdrvWuEX3W".to_string(),
pool_fee_wallet_address: test_miner_address(0x41),
..Config::default()
};
assert!(validate_pool_fee_destination_config(&cfg).is_ok());
Expand Down
11 changes: 2 additions & 9 deletions crates/pool-runtime/src/stratum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1308,14 +1308,7 @@ mod tests {
}

fn test_miner_address(seed: u8) -> String {
match seed {
0x55 => "CAWo991gTpGrbhyPsjoNBpVfFiGmWVMD69q4JX9QrJ1Q8PdFSVPdUDYXwAQQV9DYK3gm2J1r33vZ44Jw5yC6BJcf7wT9w",
0x61 => "DjvnqBWSFE1MSH3wmFizcYgXgzLMma2ugzqhcgrrjc6v1FDi3Ud4QZ4EU1Vkrrdq1ojeGoiDyFFiMYkfXEdYA2gHHTCEx",
0x62 => "DsYd923qEB9jAzUVRP3xpGs6tmAzTVbDaEbFj319PiShzegkG4PgKR1sgFFhizvMQY5DYWrRTvs9JAxUZLkVVFrDr2R49",
0x71 => "Fqp7kaAmxSKMDhpgHGxVrBbgwMkUnLwqpnrZNF9TFMZbqigKWnvxzLQVqocth9CDHV8pc9yPtB2bRY1e6vYoTfRt5jGPt",
_ => panic!("missing test address for seed {seed:#x}"),
}
.to_string()
crate::test_support::test_miner_address(seed)
}

async fn build_tcp_test_server(cfg: Config) -> (Arc<StratumServer>, Arc<JobManager>) {
Expand Down Expand Up @@ -1724,7 +1717,7 @@ mod tests {
Arc::new(RuntimeStartingStore),
Arc::new(InMemoryNode::default()),
));
let address = "9w1duuowmepV5Zn7hbEtkTPvoa21onsy584eSciXgSCvJWhbkbK6yaEdM8XKniNdedx1REcUdSYF3Sr9UB9sYSh4iuSvT".to_string();
let address = test_miner_address(0x72);
engine
.login(
"conn1",
Expand Down
29 changes: 29 additions & 0 deletions crates/pool-runtime/src/test_support.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use curve25519_dalek::constants::RISTRETTO_BASEPOINT_TABLE;
use curve25519_dalek::scalar::Scalar;
use sha3::{Digest, Sha3_256};

const STEALTH_ADDRESS_CHECKSUM_TAG: &[u8] = b"blocknet_stealth_address_checksum";
const NETWORK_ID_MAINNET: &str = "blocknet_mainnet";

pub(crate) fn test_miner_address(seed: u8) -> String {
let spend_scalar = Scalar::from_bytes_mod_order([seed.max(1); 32]);
let view_scalar = Scalar::from_bytes_mod_order([seed.wrapping_add(1).max(1); 32]);
let spend_pub = (&spend_scalar * RISTRETTO_BASEPOINT_TABLE).compress();
let view_pub = (&view_scalar * RISTRETTO_BASEPOINT_TABLE).compress();

let mut payload = [0u8; 64];
payload[..32].copy_from_slice(spend_pub.as_bytes());
payload[32..].copy_from_slice(view_pub.as_bytes());

let mut encoded = payload.to_vec();
encoded.extend_from_slice(&address_checksum(&payload)[..4]);
bs58::encode(encoded).into_string()
}

fn address_checksum(payload: &[u8]) -> [u8; 32] {
let mut hasher = Sha3_256::new();
hasher.update(STEALTH_ADDRESS_CHECKSUM_TAG);
hasher.update(NETWORK_ID_MAINNET.as_bytes());
hasher.update(payload);
hasher.finalize().into()
}