Skip to main content

winterwallet_core/
pubkey.rs

1use core::mem::MaybeUninit;
2
3use crate::WinternitzError;
4
5/// 32-byte commitment to a [`WinternitzPubkey`] — the domain-separated Merkle
6/// root over its scalars. This is the value verifiers store; signatures are
7/// validated against it.
8#[repr(transparent)]
9#[derive(Copy, Clone, PartialEq, Eq, Debug)]
10pub struct WinternitzRoot([u8; 32]);
11
12/// Public Winternitz key: `N` message scalars followed by 2 checksum scalars,
13/// each 32 bytes. Total size is `(N + 2) * 32` bytes.
14///
15/// Each pubkey scalar equals `SHA256` applied 255 times to the corresponding
16/// privkey scalar. `N` must be even and in `16..=32` (compile-time enforced).
17#[repr(C)]
18pub struct WinternitzPubkey<const N: usize> {
19    scalars: [[u8; 32]; N],
20    checksum: [[u8; 32]; 2],
21}
22
23impl<'a, const N: usize> TryFrom<&'a [u8]> for &'a WinternitzPubkey<N> {
24    type Error = WinternitzError;
25
26    fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
27        const { crate::assert_n::<N>() };
28        if value.len() != (N + 2) * 32 {
29            return Err(WinternitzError::InvalidLength);
30        }
31        // SAFETY: length verified; alignment of WinternitzPubkey<N> is 1 (all fields are [u8; _]);
32        // every bit pattern is a valid inhabitant.
33        Ok(unsafe { &*value.as_ptr().cast::<WinternitzPubkey<N>>() })
34    }
35}
36
37impl WinternitzRoot {
38    /// Wrap a 32-byte value as a `WinternitzRoot`. No validation is performed
39    /// — the caller is asserting that these bytes are a previously-produced
40    /// root (e.g. loaded from on-chain account state).
41    pub const fn new(bytes: [u8; 32]) -> Self {
42        Self(bytes)
43    }
44
45    /// Return the 32 raw bytes of the root.
46    pub fn as_bytes(&self) -> &[u8; 32] {
47        &self.0
48    }
49}
50
51impl From<[u8; 32]> for WinternitzRoot {
52    fn from(bytes: [u8; 32]) -> Self {
53        Self(bytes)
54    }
55}
56
57impl core::fmt::Display for WinternitzRoot {
58    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
59        write!(f, "0x")?;
60        for b in &self.0 {
61            write!(f, "{:02x}", b)?;
62        }
63        Ok(())
64    }
65}
66
67impl<const N: usize> WinternitzPubkey<N> {
68    pub(crate) fn new(scalars: [[u8; 32]; N], checksum: [[u8; 32]; 2]) -> Self {
69        const { crate::assert_n::<N>() };
70        Self { scalars, checksum }
71    }
72
73    /// Return the pubkey's `(N + 2) * 32` raw bytes (message scalars then
74    /// checksum scalars), with no copy.
75    pub fn as_bytes(&self) -> &[u8] {
76        // SAFETY: #[repr(C)], all fields are [u8; _] with align 1, no padding.
77        unsafe {
78            core::slice::from_raw_parts(
79                self as *const Self as *const u8,
80                core::mem::size_of::<Self>(),
81            )
82        }
83    }
84
85    /// Compute the Merkle root over all `N + 2` scalars. Leaves are
86    /// `SHA256(0x00 || scalar)`; internal nodes are `SHA256(0x01 || L || R)`
87    /// (domain-separated). The tree shape is "rightmost-duplication on odd
88    /// levels" — equivalently, an online stack-collapse where leaves are
89    /// streamed left-to-right, adjacent same-level entries combine on push,
90    /// and any orphan top-of-stack at drain is lifted to its left
91    /// neighbour's level by repeated self-duplication.
92    pub fn merklize(&self) -> WinternitzRoot {
93        const { crate::assert_n::<N>() };
94        // Domain-separate leaves and internal nodes to prevent second-preimage
95        // attacks where an attacker constructs an internal node value that
96        // collides with a leaf hash.
97        const LEAF_TAG: &[u8] = &[0x00];
98        const NODE_TAG: &[u8] = &[0x01];
99
100        // For N in [16, 32] (even), N + 2 is in [18, 34]. Maximum stack
101        // depth across any prefix is `popcount` of that prefix length —
102        // worst case is i = 31 with popcount 5, hit only when N + 2 >= 32.
103        const MAX_DEPTH: usize = 5;
104        let mut stack: [MaybeUninit<[u8; 32]>; MAX_DEPTH] =
105            [const { MaybeUninit::uninit() }; MAX_DEPTH];
106        let mut levels = [0u8; MAX_DEPTH];
107        let mut len: usize = 0;
108
109        for leaf in self.scalars.iter().chain(self.checksum.iter()) {
110            let mut h: [u8; 32] = solana_sha256_hasher::hashv(&[LEAF_TAG, leaf]).to_bytes();
111            let mut level: u8 = 0;
112            while len > 0 && levels[len - 1] == level {
113                // SAFETY: stack[len - 1] was initialised in a prior push.
114                let top = unsafe { stack[len - 1].assume_init_read() };
115                h = solana_sha256_hasher::hashv(&[NODE_TAG, &top, &h]).to_bytes();
116                level += 1;
117                len -= 1;
118            }
119            stack[len].write(h);
120            levels[len] = level;
121            len += 1;
122        }
123
124        // Drain: collapse the stack, lifting orphan right-edge subtrees by
125        // self-duplication so the resulting tree matches the level-by-level
126        // shape with rightmost-duplication.
127        while len > 1 {
128            // SAFETY: both top entries are initialised.
129            let mut top = unsafe { stack[len - 1].assume_init_read() };
130            let mut top_level = levels[len - 1];
131            let next_level = levels[len - 2];
132            while top_level < next_level {
133                top = solana_sha256_hasher::hashv(&[NODE_TAG, &top, &top]).to_bytes();
134                top_level += 1;
135            }
136            let next = unsafe { stack[len - 2].assume_init_read() };
137            let combined = solana_sha256_hasher::hashv(&[NODE_TAG, &next, &top]).to_bytes();
138            stack[len - 2].write(combined);
139            levels[len - 2] = top_level + 1;
140            len -= 1;
141        }
142
143        // SAFETY: at least N + 2 >= 18 leaves were processed, so len == 1.
144        let root = unsafe { stack[0].assume_init_read() };
145        WinternitzRoot(root)
146    }
147}
148
149impl<const N: usize> core::fmt::Display for WinternitzPubkey<N> {
150    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
151        write!(f, "0x")?;
152        for s in self.scalars.iter().chain(self.checksum.iter()) {
153            for b in s {
154                write!(f, "{:02x}", b)?;
155            }
156        }
157        Ok(())
158    }
159}
160
161impl<const N: usize> core::fmt::Debug for WinternitzPubkey<N> {
162    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
163        writeln!(f, "WinternitzPubkey {{")?;
164        for (i, s) in self.scalars.iter().enumerate() {
165            write!(f, "  scalars[{}]  = 0x", i)?;
166            for b in s {
167                write!(f, "{:02x}", b)?;
168            }
169            writeln!(f)?;
170        }
171        for (i, s) in self.checksum.iter().enumerate() {
172            write!(f, "  checksum[{}] = 0x", i)?;
173            for b in s {
174                write!(f, "{:02x}", b)?;
175            }
176            writeln!(f)?;
177        }
178        write!(f, "}}")
179    }
180}
181
182impl<const N: usize> From<WinternitzPubkey<N>> for WinternitzRoot {
183    fn from(pk: WinternitzPubkey<N>) -> Self {
184        const { crate::assert_n::<N>() };
185        pk.merklize()
186    }
187}
188
189impl<const N: usize> From<&WinternitzPubkey<N>> for WinternitzRoot {
190    fn from(pk: &WinternitzPubkey<N>) -> Self {
191        const { crate::assert_n::<N>() };
192        pk.merklize()
193    }
194}