Skip to main content

wots_rhdl/
digits.rs

1//! Fixed-work message-digit extraction and signature-capture selection.
2//!
3//! The WOTS controller always advances all 67 chains through positions 0..15.
4//! These kernels only decide which already-produced position is copied to the
5//! signature store; message digits never suppress hash work.
6
7use rhdl::prelude::*;
8
9/// Bytes in the already-hashed message accepted by WOTS.
10pub const MESSAGE_BYTES: usize = 32;
11
12/// Base-16 digits contributed directly by the 256-bit message.
13pub const MESSAGE_DIGITS: usize = 64;
14
15/// Base-16 checksum digits appended after the message digits.
16pub const CHECKSUM_DIGITS: usize = 3;
17
18/// Total number of WOTS chains and message/checksum digits.
19pub const CHAIN_COUNT: usize = MESSAGE_DIGITS + CHECKSUM_DIGITS;
20
21/// Byte-oriented message-digest representation used by RHDL.
22pub type MessageBytes = [b8; MESSAGE_BYTES];
23
24/// One base-16 target position for each WOTS chain.
25pub type MessageDigitArray = [b4; CHAIN_COUNT];
26
27/// Input to [`signature_capture_kernel`].
28#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
29pub struct SignatureCaptureInput {
30    /// Target position derived from the message and checksum.
31    pub message_digit: b4,
32    /// Current chain position, including the unhashed secret at position zero.
33    pub chain_position: b4,
34}
35
36/// Convert a 32-byte digest to 64 high-then-low nibbles plus checksum.
37///
38/// The checksum is `sum(15 - digit)` and is emitted as three big-endian
39/// base-16 digits. Its maximum value is 960, so ten bits are sufficient.
40#[kernel]
41pub fn message_digits_kernel(message: MessageBytes) -> MessageDigitArray {
42    let mut digits = [b4(0); CHAIN_COUNT];
43    let mut checksum = b10(0);
44    for byte_index in 0..MESSAGE_BYTES {
45        let high: b4 = (message[byte_index] >> 4).resize();
46        let low: b4 = message[byte_index].resize();
47        digits[byte_index * 2] = high;
48        digits[byte_index * 2 + 1] = low;
49        let high_wide: b10 = high.resize();
50        let low_wide: b10 = low.resize();
51        checksum = checksum + (b10(15) - high_wide) + (b10(15) - low_wide);
52    }
53    digits[64] = (checksum >> 8).resize();
54    digits[65] = (checksum >> 4).resize();
55    digits[66] = checksum.resize();
56    digits
57}
58
59/// Select exactly the chain position copied to the signature store.
60///
61/// Position zero captures the unhashed secret segment. Positions 1 through 15
62/// capture the corresponding chain-compression result.
63#[kernel]
64pub fn signature_capture_kernel(input: SignatureCaptureInput) -> bool {
65    input.chain_position == input.message_digit
66}
67
68/// Independent software oracle for [`message_digits_kernel`].
69#[must_use]
70pub fn software_message_digits(message: &[u8; MESSAGE_BYTES]) -> [u8; CHAIN_COUNT] {
71    let mut digits = [0_u8; CHAIN_COUNT];
72    for (index, byte) in message.iter().copied().enumerate() {
73        digits[index * 2] = byte >> 4;
74        digits[index * 2 + 1] = byte & 0x0f;
75    }
76    let checksum = digits[..MESSAGE_DIGITS]
77        .iter()
78        .fold(0_u16, |sum, digit| sum + u16::from(15 - digit));
79    let checksum_bytes = checksum.to_be_bytes();
80    digits[64] = checksum_bytes[0] & 0x0f;
81    digits[65] = checksum_bytes[1] >> 4;
82    digits[66] = checksum_bytes[1] & 0x0f;
83    digits
84}
85
86const _: () = {
87    assert!(MESSAGE_DIGITS == MESSAGE_BYTES * 2);
88    assert!(CHAIN_COUNT == 67);
89    assert!(15 * MESSAGE_DIGITS < 1 << 10);
90};