curvy_core/encoding.rs
1//! Byte/integer encodings used at the EdDSA boundary.
2//!
3//! EdDSA-Poseidon (`@zk-kit/eddsa-poseidon`) is **little-endian** for all
4//! buffer<->integer conversions (`leBufferToBigInt` / `leBigIntToBuffer`) - the
5//! opposite of the note cipher, which is big-endian. Keeping the two explicit
6//! here prevents accidental endianness flips.
7
8use core::str::FromStr;
9
10use num_bigint::BigUint;
11
12/// Parse a non-negative decimal string into a raw integer (no field reduction) -
13/// the boundary for the cipher key material, `sha256BigInt`, and the EdDSA message.
14pub fn dec_to_biguint(s: &str) -> BigUint {
15 BigUint::from_str(s).unwrap_or_else(|_| panic!("invalid decimal integer: {s:?}"))
16}
17
18/// Decode a hex string into bytes with the **lenient semantics of Node's
19/// `Buffer.from(hex, "hex")`** (the EdDSA private-key encoding):
20/// parse byte pairs left-to-right, stop at the first invalid hex character, and
21/// drop a trailing odd nibble. No `0x` stripping - `Buffer.from` does not strip it
22/// either (it would stop at the `x`).
23pub fn from_hex(s: &str) -> Vec<u8> {
24 let bytes = s.as_bytes();
25 let mut out = Vec::with_capacity(bytes.len() / 2);
26 let mut i = 0;
27 while i + 1 < bytes.len() {
28 match (hex_nibble(bytes[i]), hex_nibble(bytes[i + 1])) {
29 (Some(hi), Some(lo)) => {
30 out.push((hi << 4) | lo);
31 i += 2;
32 }
33 _ => break, // stop at the first invalid character (Node behaviour)
34 }
35 }
36 out
37}
38
39fn hex_nibble(b: u8) -> Option<u8> {
40 match b {
41 b'0'..=b'9' => Some(b - b'0'),
42 b'a'..=b'f' => Some(b - b'a' + 10),
43 b'A'..=b'F' => Some(b - b'A' + 10),
44 _ => None,
45 }
46}
47
48/// Little-endian bytes -> integer (`leBufferToBigInt`).
49pub fn le_bytes_to_biguint(bytes: &[u8]) -> BigUint {
50 BigUint::from_bytes_le(bytes)
51}
52
53/// Integer -> fixed 32-byte **big-endian** bytes (`bigIntToBytes(value, 32)`), used
54/// for the cipher key material and `sha256BigInt`. Packs the **raw** value with NO
55/// field reduction (left-padded with zeros). Panics if the value does not fit in 32
56/// bytes (matches the TS overflow guard at `value >= 2^256`).
57pub fn biguint_to_be_32(value: &BigUint) -> [u8; 32] {
58 let be = value.to_bytes_be();
59 assert!(
60 be.len() <= 32,
61 "biguint_to_be_32: value exceeds 32 bytes (>= 2^256)"
62 );
63 let mut out = [0u8; 32];
64 out[32 - be.len()..].copy_from_slice(&be);
65 out
66}
67
68/// Integer -> fixed-length little-endian bytes (`leBigIntToBuffer(value, len)`).
69/// Panics if the value does not fit in `len` bytes (matches the JS overflow guard).
70pub fn biguint_to_le_bytes(value: &BigUint, len: usize) -> Vec<u8> {
71 let mut out = value.to_bytes_le();
72 assert!(
73 out.len() <= len,
74 "biguint_to_le_bytes: value exceeds {len} bytes"
75 );
76 out.resize(len, 0);
77 out
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn hex_matches_node_buffer_from() {
86 assert_eq!(from_hex("00ff10"), vec![0x00, 0xff, 0x10]);
87 assert_eq!(from_hex("abc"), vec![0xab]); // odd: trailing nibble dropped
88 assert_eq!(from_hex("zz"), Vec::<u8>::new()); // invalid: stop immediately
89 assert_eq!(from_hex("0xab"), Vec::<u8>::new()); // no 0x strip (stops at 'x')
90 assert_eq!(from_hex(""), Vec::<u8>::new());
91 }
92
93 #[test]
94 fn le_conversions() {
95 // 0x0102 little-endian = bytes [0x02, 0x01, 0, 0]
96 let v = BigUint::from(0x0102u32);
97 assert_eq!(biguint_to_le_bytes(&v, 4), vec![0x02, 0x01, 0x00, 0x00]);
98 assert_eq!(le_bytes_to_biguint(&[0x02, 0x01, 0x00, 0x00]), v);
99 }
100}