use alloc::vec::Vec;
use hkdf::Hkdf;
use sha2::{Digest, Sha256};
pub const PROPAGATION_STAMP_SIZE: usize = 32;
pub const DEFAULT_PROPAGATION_STAMP_COST: u32 = 16;
const PROPAGATION_WORKBLOCK_EXPAND_ROUNDS: usize = 1000;
pub const MAX_STAMP_COST: u32 = 256;
const LXMF_OVERHEAD: usize = (2 * 16) + 64 + 8 + 8;
pub fn generate_propagation_stamp(transient_id: &[u8; 32], stamp_cost: u32) -> Option<Vec<u8>> {
if stamp_cost > MAX_STAMP_COST {
return None;
}
let workblock = stamp_workblock(transient_id, PROPAGATION_WORKBLOCK_EXPAND_ROUNDS);
let mut workblock_hasher = Sha256::new();
workblock_hasher.update(&workblock);
let mut stamp = alloc::vec![0u8; PROPAGATION_STAMP_SIZE];
let mut nonce = 0u64;
loop {
stamp[..8].copy_from_slice(&nonce.to_le_bytes());
if stamp_value_with_prefix(&workblock_hasher, &stamp) >= stamp_cost {
return Some(stamp);
}
nonce = nonce.wrapping_add(1);
if nonce == 0 {
return None;
}
}
}
pub fn validate_propagation_stamp(transient_data: &[u8], target_cost: u32) -> Option<u32> {
if target_cost > MAX_STAMP_COST {
return None;
}
if transient_data.len() <= LXMF_OVERHEAD + PROPAGATION_STAMP_SIZE {
return None;
}
let (lxm_data, stamp) = transient_data.split_at(transient_data.len() - PROPAGATION_STAMP_SIZE);
let transient_id = Sha256::digest(lxm_data);
let workblock = stamp_workblock(transient_id.as_slice(), PROPAGATION_WORKBLOCK_EXPAND_ROUNDS);
if stamp_valid(stamp, target_cost, &workblock) {
Some(stamp_value(&workblock, stamp))
} else {
None
}
}
fn stamp_workblock(material: &[u8], expand_rounds: usize) -> Vec<u8> {
let mut workblock = Vec::with_capacity(expand_rounds * 256);
for n in 0..expand_rounds {
let mut salt_data = Vec::with_capacity(material.len() + 8);
salt_data.extend_from_slice(material);
let packed = rmp_serde::to_vec(&n).expect("msgpack encode LXMF stamp workblock round");
salt_data.extend_from_slice(&packed);
let salt_hash = Sha256::digest(&salt_data);
let hk = Hkdf::<Sha256>::new(Some(salt_hash.as_slice()), material);
let mut okm = [0u8; 256];
hk.expand(&[], &mut okm).expect("hkdf expand for LXMF stamp workblock");
workblock.extend_from_slice(&okm);
}
workblock
}
fn stamp_valid(stamp: &[u8], target_cost: u32, workblock: &[u8]) -> bool {
stamp_value(workblock, stamp) >= target_cost
}
fn stamp_value(workblock: &[u8], stamp: &[u8]) -> u32 {
let mut hasher = Sha256::new();
hasher.update(workblock);
hasher.update(stamp);
stamp_value_from_hash(hasher.finalize().as_slice())
}
fn stamp_value_with_prefix(workblock_hasher: &Sha256, stamp: &[u8]) -> u32 {
let mut hasher = workblock_hasher.clone();
hasher.update(stamp);
stamp_value_from_hash(hasher.finalize().as_slice())
}
fn stamp_value_from_hash(hash: &[u8]) -> u32 {
let mut value = 0u32;
for byte in hash {
if *byte == 0 {
value += 8;
} else {
value += byte.leading_zeros();
break;
}
}
value
}
#[cfg(test)]
mod tests {
use super::{
generate_propagation_stamp, validate_propagation_stamp, DEFAULT_PROPAGATION_STAMP_COST,
PROPAGATION_STAMP_SIZE,
};
use sha2::{Digest, Sha256};
fn sha256_array(data: &[u8]) -> [u8; 32] {
let digest = Sha256::digest(data);
let mut out = [0u8; 32];
out.copy_from_slice(digest.as_slice());
out
}
#[test]
fn default_propagation_stamp_cost_matches_python_lxmrouter_default() {
assert_eq!(DEFAULT_PROPAGATION_STAMP_COST, 16);
}
#[test]
#[cfg_attr(miri, ignore = "proof-of-work expansion is prohibitively slow under Miri")]
fn unattainable_stamp_costs_are_rejected_before_mining() {
let transient_id = sha256_array(b"unattainable");
assert!(generate_propagation_stamp(&transient_id, 257).is_none());
assert!(generate_propagation_stamp(&transient_id, u32::MAX).is_none());
let lxm_data = alloc::vec![0x42u8; 160];
let transient_id = sha256_array(&lxm_data);
let stamp = generate_propagation_stamp(&transient_id, 1).expect("stamp");
let mut transient = lxm_data;
transient.extend_from_slice(&stamp);
assert!(validate_propagation_stamp(&transient, 257).is_none());
}
#[test]
#[cfg_attr(miri, ignore = "proof-of-work expansion is prohibitively slow under Miri")]
fn generated_propagation_stamp_validates_at_default_minimum_accepted_cost() {
let lxm_data = alloc::vec![0x42u8; 160];
let transient_id = sha256_array(&lxm_data);
let stamp = generate_propagation_stamp(&transient_id, DEFAULT_PROPAGATION_STAMP_COST)
.expect("stamp generation succeeds for realistic costs");
assert_eq!(stamp.len(), PROPAGATION_STAMP_SIZE);
let mut transient = lxm_data;
transient.extend_from_slice(&stamp);
let value = validate_propagation_stamp(&transient, 13)
.expect("stamp at default target cost passes the default minimum accepted cost");
assert!(value >= DEFAULT_PROPAGATION_STAMP_COST);
}
#[test]
#[cfg_attr(miri, ignore = "proof-of-work expansion is prohibitively slow under Miri")]
fn all_zero_propagation_stamp_does_not_reliably_reach_enforced_costs() {
let lxm_data = alloc::vec![0x42u8; 160];
let mut transient = lxm_data;
transient.extend_from_slice(&[0u8; PROPAGATION_STAMP_SIZE]);
assert!(validate_propagation_stamp(&transient, 13).is_none());
}
#[test]
#[cfg_attr(miri, ignore = "proof-of-work expansion is prohibitively slow under Miri")]
fn propagation_stamp_validator_rejects_short_or_modified_payloads() {
let short = alloc::vec![0u8; 64 + PROPAGATION_STAMP_SIZE];
assert!(validate_propagation_stamp(&short, 1).is_none());
let lxm_data = alloc::vec![0x33u8; 160];
let transient_id = sha256_array(&lxm_data);
let stamp = generate_propagation_stamp(&transient_id, 1).expect("stamp");
let mut transient = lxm_data;
transient.extend_from_slice(&stamp);
transient[0] ^= 0x01;
assert!(validate_propagation_stamp(&transient, 1).is_none());
}
}