use alloc::vec::Vec;
use crate::abi::{
collect_hash_words, encode_bytes, encode_dynamic_array, encode_tuple, word_from_u32, AbiReader,
Field,
};
use crate::hash::hash_node;
use crate::profiles::NUM_WOTS_CHAINS;
use crate::HASH_LEN;
pub(crate) use crate::profiles::WOTS_C_MAX_GRIND_COUNTER;
pub(crate) const NUM_CHAINS: usize = NUM_WOTS_CHAINS as usize;
pub(crate) const BASE: u32 = crate::profiles::WOTS_CHAIN_LEN as u32;
pub(crate) const TARGET_SUM: u32 = crate::profiles::WOTS_TARGET_SUM;
#[derive(Clone, Copy)]
pub(crate) struct ChainWalk {
pub value: [u8; HASH_LEN],
pub start: u32,
pub steps: u32,
}
pub(crate) fn wots_chain_walk(
tag: &[u8],
pk_seed: &[u8; HASH_LEN],
address_word: impl Fn(u32) -> [u8; HASH_LEN],
walk: ChainWalk,
) -> [u8; HASH_LEN] {
let mut out = walk.value;
for step_offset in 0..walk.steps {
let step = walk.start + step_offset;
let addr = address_word(step);
out = hash_node(&[tag, pk_seed.as_ref(), addr.as_ref(), out.as_ref()]);
}
out
}
#[cfg(not(feature = "parallel"))]
pub(crate) fn lowest_winning_counter<T>(
limit: u32,
try_counter: impl Fn(u32) -> Option<T>,
) -> Option<(u32, T)> {
for counter in 0..limit {
if let Some(t) = try_counter(counter) {
return Some((counter, t));
}
}
None
}
#[cfg(feature = "parallel")]
pub(crate) fn lowest_winning_counter<T: Send>(
limit: u32,
try_counter: impl Fn(u32) -> Option<T> + Sync,
) -> Option<(u32, T)> {
use rayon::prelude::*;
(0..limit)
.into_par_iter()
.find_map_first(|counter| try_counter(counter).map(|t| (counter, t)))
}
#[cfg(not(feature = "parallel"))]
pub(crate) fn grind_digit_sum<D, B, C>(
max_counter: u32,
target_sum: u32,
digits_from_counter: D,
build_chains: B,
) -> Option<(u32, C)>
where
D: Fn(u32) -> Option<(u32, Vec<u32>)>,
B: Fn(&[u32]) -> C,
{
lowest_winning_counter(max_counter, |counter| {
let (digit_sum, digits) = digits_from_counter(counter)?;
(digit_sum == target_sum).then(|| build_chains(&digits))
})
}
#[cfg(feature = "parallel")]
pub(crate) fn grind_digit_sum<D, B, C>(
max_counter: u32,
target_sum: u32,
digits_from_counter: D,
build_chains: B,
) -> Option<(u32, C)>
where
D: Fn(u32) -> Option<(u32, Vec<u32>)> + Sync,
B: Fn(&[u32]) -> C + Sync,
C: Send,
{
lowest_winning_counter(max_counter, |counter| {
let (digit_sum, digits) = digits_from_counter(counter)?;
(digit_sum == target_sum).then(|| build_chains(&digits))
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signature {
pub randomizer: [u8; HASH_LEN],
pub counter: u32,
pub chains: Vec<[u8; HASH_LEN]>,
}
impl Signature {
pub fn to_bytes(&self) -> Vec<u8> {
encode_tuple(alloc::vec![
Field::Dynamic(encode_bytes(&self.randomizer)),
Field::Static(word_from_u32(self.counter)),
Field::Dynamic(encode_dynamic_array(
self.chains.iter().map(|node| encode_bytes(node)).collect(),
)),
])
}
pub(crate) fn decode(reader: &AbiReader, base: usize) -> Option<Self> {
Some(Self {
randomizer: reader.decode_bytes32_field(base, base)?,
counter: reader.read_u32(base.checked_add(32)?)?,
chains: collect_hash_words(reader.decode_array_bytes(
base,
base.checked_add(64)?,
NUM_WOTS_CHAINS as usize,
)?)?,
})
}
pub fn from_bytes(data: &[u8]) -> Option<Self> {
let reader = AbiReader::new(data);
let decoded = Self::decode(&reader, 0)?;
reader.finish()?;
Some(decoded)
}
}
impl TryFrom<&[u8]> for Signature {
type Error = ();
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Self::from_bytes(value).ok_or(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn sample_signature() -> Signature {
Signature {
randomizer: [0x5A; HASH_LEN],
counter: 0x0BAD_F00D,
chains: vec![[0x11; HASH_LEN], [0x22; HASH_LEN], [0x33; HASH_LEN]],
}
}
#[test]
fn to_bytes_from_bytes_round_trips() {
let signature = sample_signature();
let encoded = signature.to_bytes();
let decoded = Signature::from_bytes(&encoded).expect("valid encoding must decode");
assert_eq!(decoded, signature);
assert_eq!(decoded.to_bytes(), encoded);
}
#[test]
fn from_bytes_rejects_trailing_bytes() {
let mut encoded = sample_signature().to_bytes();
encoded.push(0x00);
assert!(Signature::from_bytes(&encoded).is_none());
}
fn test_address_word(step: u32) -> [u8; HASH_LEN] {
let mut word = [0u8; HASH_LEN];
word[..4].copy_from_slice(&step.to_be_bytes());
word
}
#[test]
fn chain_walk_is_deterministic() {
let pk_seed = [0x11u8; HASH_LEN];
let walk = ChainWalk {
value: [0x22u8; HASH_LEN],
start: 0,
steps: 5,
};
let a = wots_chain_walk(b"test-chain", &pk_seed, test_address_word, walk);
let b = wots_chain_walk(b"test-chain", &pk_seed, test_address_word, walk);
assert_eq!(a, b);
}
#[test]
fn chain_walk_composes_across_a_midpoint() {
let pk_seed = [0x33u8; HASH_LEN];
let value = [0x44u8; HASH_LEN];
let direct = wots_chain_walk(
b"test-chain",
&pk_seed,
test_address_word,
ChainWalk {
value,
start: 0,
steps: 7,
},
);
let midpoint = wots_chain_walk(
b"test-chain",
&pk_seed,
test_address_word,
ChainWalk {
value,
start: 0,
steps: 3,
},
);
let composed = wots_chain_walk(
b"test-chain",
&pk_seed,
test_address_word,
ChainWalk {
value: midpoint,
start: 3,
steps: 4,
},
);
assert_eq!(direct, composed);
}
#[test]
fn chain_walk_endpoint_differs_by_step_count_and_is_a_no_op_at_zero() {
let pk_seed = [0x55u8; HASH_LEN];
let value = [0x66u8; HASH_LEN];
let none = wots_chain_walk(
b"test-chain",
&pk_seed,
test_address_word,
ChainWalk {
value,
start: 0,
steps: 0,
},
);
assert_eq!(none, value, "zero steps must return the input unchanged");
let short = wots_chain_walk(
b"test-chain",
&pk_seed,
test_address_word,
ChainWalk {
value,
start: 0,
steps: 4,
},
);
let full = wots_chain_walk(
b"test-chain",
&pk_seed,
test_address_word,
ChainWalk {
value,
start: 0,
steps: 5,
},
);
assert_ne!(
short, full,
"a different step count must reach a different endpoint"
);
}
}