Skip to content

Commit d3e1ab2

Browse files
authored
Fix Fr scalar field to reduce modulo r on construction (#1758)
### 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 df56ce8 commit d3e1ab2

File tree

4 files changed

+283
-26
lines changed

4 files changed

+283
-26
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 into_bytes(self) -> BytesN<$size> {
114114
self.0
115115
}
@@ -127,7 +127,7 @@ macro_rules! impl_bytesn_repr {
127127
}
128128

129129
pub fn from_array(env: &Env, array: &[u8; $size]) -> Self {
130-
Self(<BytesN<$size>>::from_array(env, array))
130+
Self::from_bytes(BytesN::from_array(env, array))
131131
}
132132

133133
pub fn as_val(&self) -> &Val {
@@ -151,8 +151,8 @@ macro_rules! impl_bytesn_repr {
151151
type Error = ConversionError;
152152

153153
fn try_from_val(env: &Env, val: &Val) -> Result<Self, Self::Error> {
154-
let bytes = <BytesN<$size>>::try_from_val(env, val)?;
155-
Ok($elem(bytes))
154+
let bytes = BytesN::try_from_val(env, val)?;
155+
Ok(Self::from_bytes(bytes))
156156
}
157157
}
158158

@@ -226,6 +226,21 @@ macro_rules! impl_bytesn_repr {
226226
};
227227
}
228228

229+
/// Generates all `BytesN` wrapper methods and trait impls including a default
230+
/// `from_bytes` that wraps the bytes without validation.
231+
#[macro_export]
232+
macro_rules! impl_bytesn_repr {
233+
($elem: ident, $size: expr) => {
234+
impl $elem {
235+
pub fn from_bytes(bytes: BytesN<$size>) -> Self {
236+
Self(bytes)
237+
}
238+
}
239+
240+
impl_bytesn_repr_without_from_bytes!($elem, $size);
241+
};
242+
}
243+
229244
/// Bytes is a contiguous growable array type containing `u8`s.
230245
///
231246
/// The array is stored in the Host and available to the Guest through the

soroban-sdk/src/crypto/bls12_381.rs

Lines changed: 241 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

@@ -773,6 +837,7 @@ impl Bls12_381 {
773837
#[cfg(test)]
774838
mod test {
775839
use super::*;
840+
use crate::bytesn;
776841

777842
#[test]
778843
fn test_g1affine_to_val() {
@@ -839,4 +904,173 @@ mod test {
839904

840905
assert_eq!(fr, rt);
841906
}
907+
908+
#[test]
909+
fn test_fr_eq_both_unreduced() {
910+
let env = Env::default();
911+
let r = fr_modulus(&env);
912+
let one = U256::from_u32(&env, 1);
913+
914+
let a = Fr::from_u256(r.add(&one));
915+
let b = Fr::from_u256(one.clone());
916+
assert_eq!(a, b);
917+
918+
let two_r_plus_one = r.add(&r).add(&one);
919+
let c = Fr::from_u256(two_r_plus_one);
920+
assert_eq!(a, c);
921+
assert_eq!(b, c);
922+
}
923+
924+
#[test]
925+
fn test_fr_eq_unreduced_vs_zero() {
926+
let env = Env::default();
927+
let r = fr_modulus(&env);
928+
let zero = U256::from_u32(&env, 0);
929+
930+
let a = Fr::from_u256(r);
931+
let b = Fr::from_u256(zero);
932+
assert_eq!(a, b);
933+
}
934+
935+
#[test]
936+
fn test_fr_reduced_value_unchanged() {
937+
let env = Env::default();
938+
let r = fr_modulus(&env);
939+
let val = r.sub(&U256::from_u32(&env, 1));
940+
941+
let fr = Fr::from_u256(val.clone());
942+
assert_eq!(fr.to_u256(), val);
943+
944+
let fr42 = Fr::from_u256(U256::from_u32(&env, 42));
945+
assert_eq!(fr42.to_u256(), U256::from_u32(&env, 42));
946+
}
947+
948+
#[test]
949+
fn test_fr_from_bytes_reduces() {
950+
let env = Env::default();
951+
let one_fr = Fr::from_u256(U256::from_u32(&env, 1));
952+
953+
// BLS12-381 r+1 as big-endian bytes
954+
let fr_from_bytes = Fr::from_bytes(bytesn!(
955+
&env,
956+
0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000002
957+
));
958+
assert_eq!(fr_from_bytes, one_fr);
959+
}
960+
961+
#[test]
962+
fn test_fr_try_from_val_reduces() {
963+
let env = Env::default();
964+
let r = fr_modulus(&env);
965+
let one = U256::from_u32(&env, 1);
966+
967+
let unreduced_u256 = r.add(&one);
968+
let val: Val = unreduced_u256.into_val(&env);
969+
let fr_from_val: Fr = val.into_val(&env);
970+
let fr_one = Fr::from_u256(one);
971+
assert_eq!(fr_from_val, fr_one);
972+
}
973+
974+
#[test]
975+
fn test_fr_u256_into_reduces() {
976+
let env = Env::default();
977+
let r = fr_modulus(&env);
978+
let one = U256::from_u32(&env, 1);
979+
980+
let fr: Fr = r.add(&one).into();
981+
let fr_one: Fr = one.into();
982+
assert_eq!(fr, fr_one);
983+
}
984+
985+
#[test]
986+
fn test_fr_eq_unreduced_vs_host_computed() {
987+
let env = Env::default();
988+
let bls = Bls12_381::new(&env);
989+
let r = fr_modulus(&env);
990+
let five = U256::from_u32(&env, 5);
991+
992+
let user_fr = Fr::from_u256(r.add(&five));
993+
let host_fr = bls.fr_add(
994+
&Fr::from_u256(U256::from_u32(&env, 2)),
995+
&Fr::from_u256(U256::from_u32(&env, 3)),
996+
);
997+
assert_eq!(user_fr, host_fr);
998+
}
999+
1000+
// Fp validation tests
1001+
1002+
#[test]
1003+
fn test_fp_max_valid_accepted() {
1004+
let env = Env::default();
1005+
let mut p_minus_1 = BLS12_381_FP_MODULUS_BE;
1006+
p_minus_1[FP_SERIALIZED_SIZE - 1] -= 1;
1007+
let _ = Fp::from_array(&env, &p_minus_1);
1008+
}
1009+
1010+
#[test]
1011+
#[should_panic(expected = "Bls12-381: Invalid Fp")]
1012+
fn test_fp_at_modulus_panics() {
1013+
let env = Env::default();
1014+
let _ = Fp::from_array(&env, &BLS12_381_FP_MODULUS_BE);
1015+
}
1016+
1017+
#[test]
1018+
#[should_panic(expected = "Bls12-381: Invalid Fp")]
1019+
fn test_fp_above_modulus_panics() {
1020+
let env = Env::default();
1021+
let mut above = BLS12_381_FP_MODULUS_BE;
1022+
above[FP_SERIALIZED_SIZE - 1] += 1;
1023+
let _ = Fp::from_array(&env, &above);
1024+
}
1025+
1026+
#[test]
1027+
fn test_fp_from_bytes_validates() {
1028+
let env = Env::default();
1029+
let _ = Fp::from_bytes(BytesN::from_array(&env, &[0u8; FP_SERIALIZED_SIZE]));
1030+
}
1031+
1032+
#[test]
1033+
#[should_panic(expected = "Bls12-381: Invalid Fp")]
1034+
fn test_fp_from_bytes_rejects_modulus() {
1035+
let env = Env::default();
1036+
let _ = Fp::from_bytes(BytesN::from_array(&env, &BLS12_381_FP_MODULUS_BE));
1037+
}
1038+
1039+
#[test]
1040+
#[should_panic(expected = "Bls12-381: Invalid Fp")]
1041+
fn test_fp_try_from_val_rejects_modulus() {
1042+
let env = Env::default();
1043+
let bytes = BytesN::from_array(&env, &BLS12_381_FP_MODULUS_BE);
1044+
let val: Val = bytes.into_val(&env);
1045+
let _: Fp = val.into_val(&env);
1046+
}
1047+
1048+
#[test]
1049+
#[should_panic(expected = "Bls12-381: Invalid Fp")]
1050+
fn test_fp2_component_above_modulus_panics() {
1051+
let env = Env::default();
1052+
let mut fp2_bytes = [0u8; FP2_SERIALIZED_SIZE];
1053+
fp2_bytes[0..FP_SERIALIZED_SIZE].copy_from_slice(&BLS12_381_FP_MODULUS_BE);
1054+
let _ = Fp2::from_array(&env, &fp2_bytes);
1055+
}
1056+
1057+
#[test]
1058+
#[should_panic(expected = "Bls12-381: Invalid Fp")]
1059+
fn test_fp2_second_component_above_modulus_panics() {
1060+
let env = Env::default();
1061+
let mut fp2_bytes = [0u8; FP2_SERIALIZED_SIZE];
1062+
fp2_bytes[FP_SERIALIZED_SIZE..].copy_from_slice(&BLS12_381_FP_MODULUS_BE);
1063+
let _ = Fp2::from_array(&env, &fp2_bytes);
1064+
}
1065+
1066+
#[test]
1067+
fn test_fp2_max_valid_accepted() {
1068+
let env = Env::default();
1069+
let mut p_minus_1 = BLS12_381_FP_MODULUS_BE;
1070+
p_minus_1[FP_SERIALIZED_SIZE - 1] -= 1;
1071+
let mut fp2_bytes = [0u8; FP2_SERIALIZED_SIZE];
1072+
fp2_bytes[0..FP_SERIALIZED_SIZE].copy_from_slice(&p_minus_1);
1073+
fp2_bytes[FP_SERIALIZED_SIZE..].copy_from_slice(&p_minus_1);
1074+
let _ = Fp2::from_array(&env, &fp2_bytes);
1075+
}
8421076
}

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)