Skip to content

Commit 34f7f53

Browse files
authored
Fix Fr scalar field to reduce modulo r on construction (#1757)
### What `Fr` types for BLS12-381 stored raw `U256` values without modular reduction, causing mathematically equal field elements (e.g., 1 and r+1) to compare as not-equal via PartialEq. The fix: apply `rem_euclid(r)` in `From<U256>` for `Fr`, and ensuring all public construction paths produce canonical representations in [0, r). Also introduces modulus validation for BLS12-381 base-field element wrappers (Fp/Fp2) and refactor `BytesN` wrapper macros to support validated `from_bytes`.
1 parent 07dc510 commit 34f7f53

File tree

3 files changed

+279
-17
lines changed

3 files changed

+279
-17
lines changed

soroban-sdk/src/bytes.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,14 @@ macro_rules! bytesn {
102102
};
103103
}
104104

105+
/// Internal macro that generates all `BytesN` wrapper methods and trait impls
106+
/// *except* `from_bytes`. Types using this macro must provide their own
107+
/// `from_bytes(BytesN<$size>) -> Self` (e.g. to add validation).
108+
#[doc(hidden)]
105109
#[macro_export]
106-
macro_rules! impl_bytesn_repr {
110+
macro_rules! impl_bytesn_repr_without_from_bytes {
107111
($elem: ident, $size: expr) => {
108112
impl $elem {
109-
pub fn from_bytes(bytes: BytesN<$size>) -> Self {
110-
Self(bytes)
111-
}
112-
113113
pub fn to_bytes(&self) -> BytesN<$size> {
114114
self.0.clone()
115115
}
@@ -123,7 +123,7 @@ macro_rules! impl_bytesn_repr {
123123
}
124124

125125
pub fn from_array(env: &Env, array: &[u8; $size]) -> Self {
126-
Self(<BytesN<$size>>::from_array(env, array))
126+
Self::from_bytes(BytesN::from_array(env, array))
127127
}
128128

129129
pub fn as_val(&self) -> &Val {
@@ -147,8 +147,8 @@ macro_rules! impl_bytesn_repr {
147147
type Error = ConversionError;
148148

149149
fn try_from_val(env: &Env, val: &Val) -> Result<Self, Self::Error> {
150-
let bytes = <BytesN<$size>>::try_from_val(env, val)?;
151-
Ok($elem(bytes))
150+
let bytes = BytesN::try_from_val(env, val)?;
151+
Ok(Self::from_bytes(bytes))
152152
}
153153
}
154154

@@ -214,6 +214,21 @@ macro_rules! impl_bytesn_repr {
214214
};
215215
}
216216

217+
/// Generates all `BytesN` wrapper methods and trait impls including a default
218+
/// `from_bytes` that wraps the bytes without validation.
219+
#[macro_export]
220+
macro_rules! impl_bytesn_repr {
221+
($elem: ident, $size: expr) => {
222+
impl $elem {
223+
pub fn from_bytes(bytes: BytesN<$size>) -> Self {
224+
Self(bytes)
225+
}
226+
}
227+
228+
impl_bytesn_repr_without_from_bytes!($elem, $size);
229+
};
230+
}
231+
217232
/// Bytes is a contiguous growable array type containing `u8`s.
218233
///
219234
/// The array is stored in the Host and available to the Guest through the

soroban-sdk/src/crypto/bls12_381.rs

Lines changed: 246 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
use crate::xdr::ScVal;
33
use crate::{
44
env::internal::{self, BytesObject, U256Val, U64Val},
5-
impl_bytesn_repr,
5+
impl_bytesn_repr_without_from_bytes,
66
unwrap::{UnwrapInfallible, UnwrapOptimized},
77
Bytes, BytesN, ConversionError, Env, IntoVal, TryFromVal, Val, Vec, U256,
88
};
@@ -162,10 +162,55 @@ pub struct Fp2(BytesN<FP2_SERIALIZED_SIZE>);
162162
#[repr(transparent)]
163163
pub struct Fr(U256);
164164

165-
impl_bytesn_repr!(G1Affine, G1_SERIALIZED_SIZE);
166-
impl_bytesn_repr!(G2Affine, G2_SERIALIZED_SIZE);
167-
impl_bytesn_repr!(Fp, FP_SERIALIZED_SIZE);
168-
impl_bytesn_repr!(Fp2, FP2_SERIALIZED_SIZE);
165+
impl_bytesn_repr_without_from_bytes!(G1Affine, G1_SERIALIZED_SIZE);
166+
impl_bytesn_repr_without_from_bytes!(G2Affine, G2_SERIALIZED_SIZE);
167+
impl_bytesn_repr_without_from_bytes!(Fp, FP_SERIALIZED_SIZE);
168+
impl_bytesn_repr_without_from_bytes!(Fp2, FP2_SERIALIZED_SIZE);
169+
170+
// BLS12-381 base field modulus p in big-endian bytes.
171+
// p = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab
172+
const BLS12_381_FP_MODULUS_BE: [u8; FP_SERIALIZED_SIZE] = [
173+
0x1a, 0x01, 0x11, 0xea, 0x39, 0x7f, 0xe6, 0x9a, 0x4b, 0x1b, 0xa7, 0xb6, 0x43, 0x4b, 0xac, 0xd7,
174+
0x64, 0x77, 0x4b, 0x84, 0xf3, 0x85, 0x12, 0xbf, 0x67, 0x30, 0xd2, 0xa0, 0xf6, 0xb0, 0xf6, 0x24,
175+
0x1e, 0xab, 0xff, 0xfe, 0xb1, 0x53, 0xff, 0xff, 0xb9, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xaa, 0xab,
176+
];
177+
178+
fn validate_fp(bytes: &[u8; FP_SERIALIZED_SIZE]) {
179+
if bytes >= &BLS12_381_FP_MODULUS_BE {
180+
sdk_panic!("Bls12-381: Invalid Fp");
181+
}
182+
}
183+
184+
fn validate_fp2(bytes: &[u8; FP2_SERIALIZED_SIZE]) {
185+
validate_fp(bytes[0..FP_SERIALIZED_SIZE].try_into().unwrap());
186+
validate_fp(bytes[FP_SERIALIZED_SIZE..].try_into().unwrap());
187+
}
188+
189+
impl G1Affine {
190+
pub fn from_bytes(bytes: BytesN<G1_SERIALIZED_SIZE>) -> Self {
191+
Self(bytes)
192+
}
193+
}
194+
195+
impl G2Affine {
196+
pub fn from_bytes(bytes: BytesN<G2_SERIALIZED_SIZE>) -> Self {
197+
Self(bytes)
198+
}
199+
}
200+
201+
impl Fp {
202+
pub fn from_bytes(bytes: BytesN<FP_SERIALIZED_SIZE>) -> Self {
203+
validate_fp(&bytes.to_array());
204+
Self(bytes)
205+
}
206+
}
207+
208+
impl Fp2 {
209+
pub fn from_bytes(bytes: BytesN<FP2_SERIALIZED_SIZE>) -> Self {
210+
validate_fp2(&bytes.to_array());
211+
Self(bytes)
212+
}
213+
}
169214

170215
impl Fp {
171216
pub fn env(&self) -> &Env {
@@ -476,9 +521,28 @@ impl Fr {
476521
}
477522
}
478523

524+
// BLS12-381 scalar field modulus r in big-endian bytes.
525+
// r = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001
526+
const BLS12_381_FR_MODULUS_BE: [u8; 32] = [
527+
0x73, 0xed, 0xa7, 0x53, 0x29, 0x9d, 0x7d, 0x48, 0x33, 0x39, 0xd8, 0x08, 0x09, 0xa1, 0xd8, 0x05,
528+
0x53, 0xbd, 0xa4, 0x02, 0xff, 0xfe, 0x5b, 0xfe, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01,
529+
];
530+
531+
fn fr_modulus(env: &Env) -> U256 {
532+
U256::from_be_bytes(env, &Bytes::from_array(env, &BLS12_381_FR_MODULUS_BE))
533+
}
534+
479535
impl From<U256> for Fr {
480536
fn from(value: U256) -> Self {
481-
Self(value)
537+
// Keep all Fr construction paths canonical by reducing modulo r here.
538+
// Skip the expensive rem_euclid when value is already canonical (< r),
539+
// which is always the case for host-returned arithmetic results.
540+
let modulus = fr_modulus(value.env());
541+
if value >= modulus {
542+
Self(value.rem_euclid(&modulus))
543+
} else {
544+
Self(value)
545+
}
482546
}
483547
}
484548

@@ -493,7 +557,7 @@ impl TryFromVal<Env, Val> for Fr {
493557

494558
fn try_from_val(env: &Env, val: &Val) -> Result<Self, Self::Error> {
495559
let u = U256::try_from_val(env, val)?;
496-
Ok(Fr(u))
560+
Ok(u.into())
497561
}
498562
}
499563

@@ -761,3 +825,178 @@ impl Bls12_381 {
761825
U256::try_from_val(env, &v).unwrap_infallible().into()
762826
}
763827
}
828+
829+
#[cfg(test)]
830+
mod test {
831+
use super::*;
832+
use crate::bytesn;
833+
834+
#[test]
835+
fn test_fr_eq_both_unreduced() {
836+
let env = Env::default();
837+
let r = fr_modulus(&env);
838+
let one = U256::from_u32(&env, 1);
839+
840+
let a = Fr::from_u256(r.add(&one));
841+
let b = Fr::from_u256(one.clone());
842+
assert_eq!(a, b);
843+
844+
let two_r_plus_one = r.add(&r).add(&one);
845+
let c = Fr::from_u256(two_r_plus_one);
846+
assert_eq!(a, c);
847+
assert_eq!(b, c);
848+
}
849+
850+
#[test]
851+
fn test_fr_eq_unreduced_vs_zero() {
852+
let env = Env::default();
853+
let r = fr_modulus(&env);
854+
let zero = U256::from_u32(&env, 0);
855+
856+
let a = Fr::from_u256(r);
857+
let b = Fr::from_u256(zero);
858+
assert_eq!(a, b);
859+
}
860+
861+
#[test]
862+
fn test_fr_reduced_value_unchanged() {
863+
let env = Env::default();
864+
let r = fr_modulus(&env);
865+
let val = r.sub(&U256::from_u32(&env, 1));
866+
867+
let fr = Fr::from_u256(val.clone());
868+
assert_eq!(fr.to_u256(), val);
869+
870+
let fr42 = Fr::from_u256(U256::from_u32(&env, 42));
871+
assert_eq!(fr42.to_u256(), U256::from_u32(&env, 42));
872+
}
873+
874+
#[test]
875+
fn test_fr_from_bytes_reduces() {
876+
let env = Env::default();
877+
let one_fr = Fr::from_u256(U256::from_u32(&env, 1));
878+
879+
// BLS12-381 r+1 as big-endian bytes
880+
let fr_from_bytes = Fr::from_bytes(bytesn!(
881+
&env,
882+
0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000002
883+
));
884+
assert_eq!(fr_from_bytes, one_fr);
885+
}
886+
887+
#[test]
888+
fn test_fr_try_from_val_reduces() {
889+
let env = Env::default();
890+
let r = fr_modulus(&env);
891+
let one = U256::from_u32(&env, 1);
892+
893+
let unreduced_u256 = r.add(&one);
894+
let val: Val = unreduced_u256.into_val(&env);
895+
let fr_from_val: Fr = val.into_val(&env);
896+
let fr_one = Fr::from_u256(one);
897+
assert_eq!(fr_from_val, fr_one);
898+
}
899+
900+
#[test]
901+
fn test_fr_u256_into_reduces() {
902+
let env = Env::default();
903+
let r = fr_modulus(&env);
904+
let one = U256::from_u32(&env, 1);
905+
906+
let fr: Fr = r.add(&one).into();
907+
let fr_one: Fr = one.into();
908+
assert_eq!(fr, fr_one);
909+
}
910+
911+
#[test]
912+
fn test_fr_eq_unreduced_vs_host_computed() {
913+
let env = Env::default();
914+
let bls = Bls12_381::new(&env);
915+
let r = fr_modulus(&env);
916+
let five = U256::from_u32(&env, 5);
917+
918+
let user_fr = Fr::from_u256(r.add(&five));
919+
let host_fr = bls.fr_add(
920+
&Fr::from_u256(U256::from_u32(&env, 2)),
921+
&Fr::from_u256(U256::from_u32(&env, 3)),
922+
);
923+
assert_eq!(user_fr, host_fr);
924+
}
925+
926+
// Fp validation tests
927+
928+
#[test]
929+
fn test_fp_max_valid_accepted() {
930+
let env = Env::default();
931+
let mut p_minus_1 = BLS12_381_FP_MODULUS_BE;
932+
p_minus_1[FP_SERIALIZED_SIZE - 1] -= 1;
933+
let _ = Fp::from_array(&env, &p_minus_1);
934+
}
935+
936+
#[test]
937+
#[should_panic(expected = "Bls12-381: Invalid Fp")]
938+
fn test_fp_at_modulus_panics() {
939+
let env = Env::default();
940+
let _ = Fp::from_array(&env, &BLS12_381_FP_MODULUS_BE);
941+
}
942+
943+
#[test]
944+
#[should_panic(expected = "Bls12-381: Invalid Fp")]
945+
fn test_fp_above_modulus_panics() {
946+
let env = Env::default();
947+
let mut above = BLS12_381_FP_MODULUS_BE;
948+
above[FP_SERIALIZED_SIZE - 1] += 1;
949+
let _ = Fp::from_array(&env, &above);
950+
}
951+
952+
#[test]
953+
fn test_fp_from_bytes_validates() {
954+
let env = Env::default();
955+
let _ = Fp::from_bytes(BytesN::from_array(&env, &[0u8; FP_SERIALIZED_SIZE]));
956+
}
957+
958+
#[test]
959+
#[should_panic(expected = "Bls12-381: Invalid Fp")]
960+
fn test_fp_from_bytes_rejects_modulus() {
961+
let env = Env::default();
962+
let _ = Fp::from_bytes(BytesN::from_array(&env, &BLS12_381_FP_MODULUS_BE));
963+
}
964+
965+
#[test]
966+
#[should_panic(expected = "Bls12-381: Invalid Fp")]
967+
fn test_fp_try_from_val_rejects_modulus() {
968+
let env = Env::default();
969+
let bytes = BytesN::from_array(&env, &BLS12_381_FP_MODULUS_BE);
970+
let val: Val = bytes.into_val(&env);
971+
let _: Fp = val.into_val(&env);
972+
}
973+
974+
#[test]
975+
#[should_panic(expected = "Bls12-381: Invalid Fp")]
976+
fn test_fp2_component_above_modulus_panics() {
977+
let env = Env::default();
978+
let mut fp2_bytes = [0u8; FP2_SERIALIZED_SIZE];
979+
fp2_bytes[0..FP_SERIALIZED_SIZE].copy_from_slice(&BLS12_381_FP_MODULUS_BE);
980+
let _ = Fp2::from_array(&env, &fp2_bytes);
981+
}
982+
983+
#[test]
984+
#[should_panic(expected = "Bls12-381: Invalid Fp")]
985+
fn test_fp2_second_component_above_modulus_panics() {
986+
let env = Env::default();
987+
let mut fp2_bytes = [0u8; FP2_SERIALIZED_SIZE];
988+
fp2_bytes[FP_SERIALIZED_SIZE..].copy_from_slice(&BLS12_381_FP_MODULUS_BE);
989+
let _ = Fp2::from_array(&env, &fp2_bytes);
990+
}
991+
992+
#[test]
993+
fn test_fp2_max_valid_accepted() {
994+
let env = Env::default();
995+
let mut p_minus_1 = BLS12_381_FP_MODULUS_BE;
996+
p_minus_1[FP_SERIALIZED_SIZE - 1] -= 1;
997+
let mut fp2_bytes = [0u8; FP2_SERIALIZED_SIZE];
998+
fp2_bytes[0..FP_SERIALIZED_SIZE].copy_from_slice(&p_minus_1);
999+
fp2_bytes[FP_SERIALIZED_SIZE..].copy_from_slice(&p_minus_1);
1000+
let _ = Fp2::from_array(&env, &fp2_bytes);
1001+
}
1002+
}

soroban-sdk/src/testutils/arbitrary.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,11 @@ mod objects {
699699
type Error = ConversionError;
700700

701701
fn try_from_val(env: &Env, v: &ArbitraryFp) -> Result<Self, Self::Error> {
702-
Ok(Fp::from_array(env, &v.bytes))
702+
let mut bytes = v.bytes;
703+
// Ensure the value is strictly less than the BLS12-381 base field modulus
704+
// p = 0x1a0111ea... by restricting the most significant byte.
705+
bytes[0] %= 0x1a;
706+
Ok(Fp::from_array(env, &bytes))
703707
}
704708
}
705709

@@ -717,7 +721,11 @@ mod objects {
717721
type Error = ConversionError;
718722

719723
fn try_from_val(env: &Env, v: &ArbitraryFp2) -> Result<Self, Self::Error> {
720-
Ok(Fp2::from_array(env, &v.bytes))
724+
let mut bytes = v.bytes;
725+
// Ensure both Fp components are strictly less than the modulus
726+
bytes[0] %= 0x1a;
727+
bytes[FP_SERIALIZED_SIZE] %= 0x1a;
728+
Ok(Fp2::from_array(env, &bytes))
721729
}
722730
}
723731

0 commit comments

Comments
 (0)