Skip to main content

curvy_core/
field.rs

1//! BN254 scalar field (`Fr`) - the SNARK scalar field shared by circom, snarkjs,
2//! poseidon-lite, and @zk-kit:
3//!
4//! ```text
5//! 21888242871839275222246405745257275088548364400416034343698204186575808495617
6//! ```
7//!
8//! The public API speaks **decimal strings** at the boundary; internally we use
9//! `ark_bn254::Fr`. These helpers are the single conversion point.
10
11use core::{fmt, str::FromStr};
12
13use ark_ff::{BigInteger, PrimeField};
14use num_bigint::BigUint;
15
16/// The BN254 scalar field element.
17pub use ark_bn254::Fr;
18
19/// Decimal string of the field modulus (`SNARK_SCALAR_FIELD`).
20pub const FIELD_MODULUS_DEC: &str =
21    "21888242871839275222246405745257275088548364400416034343698204186575808495617";
22
23/// A canonically parsed BN254 field element for untrusted protocol boundaries.
24///
25/// Unlike [`fr_from_dec`], this type rejects non-canonical encodings instead of
26/// reducing them modulo the field. Internally trusted arithmetic can continue to
27/// use [`Fr`] directly.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub struct Bn254Fr(Fr);
30
31/// Failure to parse a canonical BN254 field element.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub enum Bn254FrError {
34    InvalidDecimal,
35    NonCanonicalDecimal,
36    OutOfRange,
37}
38
39impl fmt::Display for Bn254FrError {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Self::InvalidDecimal => f.write_str("invalid unsigned decimal field element"),
43            Self::NonCanonicalDecimal => f.write_str("non-canonical decimal field element"),
44            Self::OutOfRange => {
45                f.write_str("field element is greater than or equal to the BN254 modulus")
46            }
47        }
48    }
49}
50
51impl std::error::Error for Bn254FrError {}
52
53impl Bn254Fr {
54    /// Parse a canonical unsigned decimal integer in `[0, p)`.
55    pub fn try_from_dec(s: &str) -> Result<Self, Bn254FrError> {
56        if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
57            return Err(Bn254FrError::InvalidDecimal);
58        }
59        if s.len() > 1 && s.starts_with('0') {
60            return Err(Bn254FrError::NonCanonicalDecimal);
61        }
62        let value = BigUint::parse_bytes(s.as_bytes(), 10).ok_or(Bn254FrError::InvalidDecimal)?;
63        let modulus =
64            BigUint::parse_bytes(FIELD_MODULUS_DEC.as_bytes(), 10).expect("valid BN254 modulus");
65        if value >= modulus {
66            return Err(Bn254FrError::OutOfRange);
67        }
68        Ok(Self(fr_from_biguint(&value)))
69    }
70
71    /// Wrap an already canonical internal field element.
72    #[inline]
73    pub fn from_fr(value: Fr) -> Self {
74        Self(value)
75    }
76
77    #[inline]
78    pub fn as_fr(&self) -> &Fr {
79        &self.0
80    }
81
82    #[inline]
83    pub fn into_inner(self) -> Fr {
84        self.0
85    }
86
87    pub fn to_dec(self) -> String {
88        fr_to_dec(&self.0)
89    }
90
91    pub fn to_le_32(self) -> [u8; 32] {
92        let bytes = fr_to_biguint(&self.0).to_bytes_le();
93        let mut out = [0u8; 32];
94        out[..bytes.len()].copy_from_slice(&bytes);
95        out
96    }
97}
98
99/// Parse a decimal string into a field element, **reducing modulo the field
100/// modulus** (`modulus + 5` → `5`, `-1` → `modulus − 1`).
101///
102/// This is deliberate: it mirrors how poseidon-lite / circom coerce inputs, so it
103/// is the correct boundary for *field-element* values (Poseidon inputs, amounts,
104/// commitments). For *raw 256-bit* integers that must NOT be reduced - the cipher
105/// key material, `sha256BigInt` inputs, and the EdDSA signing message - use a
106/// [`num_bigint::BigUint`] with the raw byte encodings in [`crate::encoding`].
107///
108/// Panics only if `s` is not a valid (optionally signed) decimal integer.
109pub fn fr_from_dec(s: &str) -> Fr {
110    Fr::from_str(s).unwrap_or_else(|_| panic!("invalid field decimal: {s:?}"))
111}
112
113/// Reduce a non-negative integer modulo the field modulus into an `Fr`.
114pub fn fr_from_biguint(v: &BigUint) -> Fr {
115    Fr::from_be_bytes_mod_order(&v.to_bytes_be())
116}
117
118/// Render a field element as a canonical non-negative decimal string.
119pub fn fr_to_dec(x: &Fr) -> String {
120    fr_to_biguint(x).to_str_radix(10)
121}
122
123/// Field element as a `BigUint` of its canonical representative in `[0, modulus)`.
124pub fn fr_to_biguint(x: &Fr) -> BigUint {
125    BigUint::from_bytes_be(&x.into_bigint().to_bytes_be())
126}
127
128/// 32-byte **big-endian** packing of a field element's canonical representative.
129/// This is the wire encoding used by the note cipher and `sha256BigInt`.
130pub fn fr_to_be_32(x: &Fr) -> [u8; 32] {
131    let be = x.into_bigint().to_bytes_be(); // BN254 Fr -> BigInt<4> -> exactly 32 bytes
132    debug_assert_eq!(be.len(), 32);
133    let mut out = [0u8; 32];
134    out.copy_from_slice(&be);
135    out
136}
137
138/// Interpret big-endian bytes as an integer reduced into the field (`mod modulus`).
139pub fn fr_from_be_bytes_mod(bytes: &[u8]) -> Fr {
140    Fr::from_be_bytes_mod_order(bytes)
141}
142
143/// Decode one canonical 32-byte big-endian field element.
144///
145/// Unlike [`fr_from_be_bytes_mod`], this rejects encodings greater than or equal
146/// to the modulus instead of silently reducing them. Use it for persisted or
147/// network-supplied tree data where non-canonical encodings indicate corruption.
148pub fn fr_from_be_32_checked(bytes: &[u8]) -> Option<Fr> {
149    if bytes.len() != 32 {
150        return None;
151    }
152    let value = Fr::from_be_bytes_mod_order(bytes);
153    (fr_to_be_32(&value).as_slice() == bytes).then_some(value)
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn decimal_roundtrip() {
162        for s in [
163            "0",
164            "1",
165            "42",
166            "21888242871839275222246405745257275088548364400416034343698204186575808495616",
167        ] {
168            assert_eq!(fr_to_dec(&fr_from_dec(s)), s);
169        }
170    }
171
172    #[test]
173    fn from_dec_reduces_out_of_range() {
174        // Documents the boundary contract: fr_from_dec reduces mod modulus rather
175        // than rejecting (matching poseidon-lite/circom coercion).
176        let p_plus_5 =
177            "21888242871839275222246405745257275088548364400416034343698204186575808495622";
178        assert_eq!(fr_from_dec(p_plus_5), Fr::from(5u64));
179        assert_eq!(fr_from_dec("-1"), -Fr::from(1u64));
180    }
181
182    #[test]
183    fn field_modulus_constant_matches_arkworks() {
184        // Catch any off-by-one in FIELD_MODULUS_DEC against arkworks' own modulus.
185        let be = <Fr as PrimeField>::MODULUS.to_bytes_be();
186        let dec = BigUint::from_bytes_be(&be).to_str_radix(10);
187        assert_eq!(dec, FIELD_MODULUS_DEC);
188    }
189
190    #[test]
191    fn canonical_binary_decoder_rejects_reduction_and_wrong_lengths() {
192        let five = fr_to_be_32(&Fr::from(5u64));
193        assert_eq!(fr_from_be_32_checked(&five), Some(Fr::from(5u64)));
194        assert_eq!(fr_from_be_32_checked(&five[..31]), None);
195
196        let modulus = <Fr as PrimeField>::MODULUS.to_bytes_be();
197        assert_eq!(fr_from_be_32_checked(&modulus), None);
198    }
199
200    #[test]
201    fn checked_field_parser_rejects_reduction_and_noncanonical_decimal() {
202        assert_eq!(Bn254Fr::try_from_dec("42").unwrap().to_dec(), "42");
203        assert_eq!(
204            Bn254Fr::try_from_dec("00"),
205            Err(Bn254FrError::NonCanonicalDecimal)
206        );
207        assert_eq!(
208            Bn254Fr::try_from_dec("-1"),
209            Err(Bn254FrError::InvalidDecimal)
210        );
211        assert_eq!(
212            Bn254Fr::try_from_dec(FIELD_MODULUS_DEC),
213            Err(Bn254FrError::OutOfRange)
214        );
215    }
216}