Skip to main content

fluentbase_runtime/syscall_handler/weierstrass/
weierstrass_decompress.rs

1/// Generic Weierstrass compress/decompress syscall handler
2use crate::syscall_handler::syscall_process_exit_code;
3use crate::RuntimeContext;
4use amcl::bls381::bls381::utils::deserialize_g1;
5use fluentbase_types::{
6    ExitCode, BLS12381_G1_COMPRESSED_SIZE, BLS12381_G1_RAW_AFFINE_SIZE,
7    SECP256K1_G1_COMPRESSED_SIZE, SECP256K1_G1_RAW_AFFINE_SIZE, SECP256R1_G1_COMPRESSED_SIZE,
8    SECP256R1_G1_RAW_AFFINE_SIZE,
9};
10use k256::elliptic_curve::{point::DecompressPoint, sec1::ToEncodedPoint, subtle::Choice};
11use num::{BigUint, Num};
12use rwasm::{StoreTr, TrapCode, Value};
13use sp1_curves::{
14    weierstrass::{bls12_381::Bls12381, secp256k1::Secp256k1, secp256r1::Secp256r1},
15    AffinePoint, CurveType, EllipticCurve,
16};
17
18pub fn syscall_secp256k1_decompress_handler(
19    ctx: &mut impl StoreTr<RuntimeContext>,
20    params: &[Value],
21    _result: &mut [Value],
22) -> Result<(), TrapCode> {
23    syscall_weierstrass_decompress_handler::<
24        Secp256k1,
25        { SECP256K1_G1_COMPRESSED_SIZE },
26        { SECP256K1_G1_RAW_AFFINE_SIZE },
27    >(ctx, params, _result)
28}
29pub fn syscall_secp256r1_decompress_handler(
30    ctx: &mut impl StoreTr<RuntimeContext>,
31    params: &[Value],
32    _result: &mut [Value],
33) -> Result<(), TrapCode> {
34    syscall_weierstrass_decompress_handler::<
35        Secp256r1,
36        { SECP256R1_G1_COMPRESSED_SIZE },
37        { SECP256R1_G1_RAW_AFFINE_SIZE },
38    >(ctx, params, _result)
39}
40pub fn syscall_bls12381_decompress_handler(
41    ctx: &mut impl StoreTr<RuntimeContext>,
42    params: &[Value],
43    _result: &mut [Value],
44) -> Result<(), TrapCode> {
45    syscall_weierstrass_decompress_handler::<
46        Bls12381,
47        { BLS12381_G1_COMPRESSED_SIZE },
48        { BLS12381_G1_RAW_AFFINE_SIZE },
49    >(ctx, params, _result)
50}
51
52fn syscall_weierstrass_decompress_handler<
53    E: EllipticCurve,
54    const COMPRESSED_SIZE: usize,
55    const DECOMPRESSED_SIZE: usize,
56>(
57    ctx: &mut impl StoreTr<RuntimeContext>,
58    params: &[Value],
59    _result: &mut [Value],
60) -> Result<(), TrapCode> {
61    let (yx_ptr, sign_bit) = (
62        params[0].i32().unwrap() as usize,
63        params[1].i32().unwrap() as u32,
64    );
65
66    let mut x_bytes_le = [0u8; COMPRESSED_SIZE];
67    ctx.memory_read(yx_ptr + COMPRESSED_SIZE, &mut x_bytes_le)?;
68
69    let yx_bytes_le = syscall_weierstrass_decompress_impl::<E, COMPRESSED_SIZE, DECOMPRESSED_SIZE>(
70        x_bytes_le, sign_bit,
71    )
72    .map_err(|exit_code| syscall_process_exit_code(ctx, exit_code))?;
73
74    ctx.memory_write(yx_ptr, &yx_bytes_le)?;
75    Ok(())
76}
77
78/// Secp256k1 point decompression.
79///
80/// # Input format
81/// - `x_bytes_le`: x-coordinate as 32 bytes in little-endian
82/// - `sign_bit`: 0 or 1, indicates which y to recover
83///
84/// # Output
85/// Affine point `[y || x]` in little-endian (96 bytes total, y first).
86///
87/// # Validation
88/// Returns `ExitCode::MalformedBuiltinParams` if:
89/// - `sign_bit > 1`
90/// - x-coordinate doesn't correspond to a valid curve point
91pub fn syscall_secp256k1_decompress_impl(
92    x_bytes_le: [u8; SECP256K1_G1_COMPRESSED_SIZE],
93    sign_bit: u32,
94) -> Result<[u8; SECP256K1_G1_RAW_AFFINE_SIZE], ExitCode> {
95    syscall_weierstrass_decompress_impl::<
96        Secp256k1,
97        { SECP256K1_G1_COMPRESSED_SIZE },
98        { SECP256K1_G1_RAW_AFFINE_SIZE },
99    >(x_bytes_le, sign_bit)
100}
101
102/// Secp256r1 point decompression.
103///
104/// # Input format
105/// - `x_bytes_le`: x-coordinate as 32 bytes in little-endian
106/// - `sign_bit`: 0 or 1, indicates which y to recover
107///
108/// # Output
109/// Affine point `[y || x]` in little-endian (64 bytes total, y first).
110///
111/// # Validation
112/// Returns `ExitCode::MalformedBuiltinParams` if:
113/// - `sign_bit > 1`
114/// - x-coordinate doesn't correspond to a valid curve point
115pub fn syscall_secp256r1_decompress_impl(
116    x_bytes_le: [u8; SECP256R1_G1_COMPRESSED_SIZE],
117    sign_bit: u32,
118) -> Result<[u8; SECP256R1_G1_RAW_AFFINE_SIZE], ExitCode> {
119    syscall_weierstrass_decompress_impl::<
120        Secp256r1,
121        { SECP256R1_G1_COMPRESSED_SIZE },
122        { SECP256R1_G1_RAW_AFFINE_SIZE },
123    >(x_bytes_le, sign_bit)
124}
125
126/// BLS12-381 point decompression.
127///
128/// # Input format
129/// - `x_bytes_le`: x-coordinate as 48 bytes in little-endian
130/// - `sign_bit`: 0 or 1, indicates which y to recover
131///
132/// # Output
133/// Affine point `[y || x]` in little-endian (96 bytes total, y first).
134///
135/// # Validation
136/// Returns `ExitCode::MalformedBuiltinParams` if:
137/// - `sign_bit > 1`
138/// - x-coordinate doesn't correspond to a valid curve point
139pub fn syscall_bls12381_decompress_impl(
140    x_bytes_le: [u8; BLS12381_G1_COMPRESSED_SIZE],
141    sign_bit: u32,
142) -> Result<[u8; BLS12381_G1_RAW_AFFINE_SIZE], ExitCode> {
143    syscall_weierstrass_decompress_impl::<
144        Bls12381,
145        { BLS12381_G1_COMPRESSED_SIZE },
146        { BLS12381_G1_RAW_AFFINE_SIZE },
147    >(x_bytes_le, sign_bit)
148}
149
150fn syscall_weierstrass_decompress_impl<
151    E: EllipticCurve,
152    const COMPRESSED_SIZE: usize,
153    const DECOMPRESSED_SIZE: usize,
154>(
155    x_bytes_le: [u8; COMPRESSED_SIZE],
156    sign_bit: u32,
157) -> Result<[u8; DECOMPRESSED_SIZE], ExitCode> {
158    if sign_bit > 1 {
159        return Err(ExitCode::MalformedBuiltinParams);
160    }
161
162    // Note: the code bellow is copied from SP1 repository as-is,
163    //  where we replaced panics with error codes to avoid node crashes.
164    //
165    // https://github.com/succinctlabs/sp1/tree/dev/crates/curves/src/weierstrass
166    let computed_point = match E::CURVE_TYPE {
167        CurveType::Secp256k1 => {
168            let mut x_bytes_be = x_bytes_le;
169            x_bytes_be.reverse();
170            let computed_point = k256::AffinePoint::decompress(
171                x_bytes_be.as_slice().into(),
172                Choice::from(sign_bit as u8),
173            );
174            if computed_point.is_none().into() {
175                return Err(ExitCode::MalformedBuiltinParams);
176            }
177            let point = computed_point.unwrap().to_encoded_point(false);
178            let x = BigUint::from_bytes_be(point.x().unwrap());
179            let y = BigUint::from_bytes_be(point.y().unwrap());
180            AffinePoint::<E>::new(x, y)
181        }
182        CurveType::Secp256r1 => {
183            let mut x_bytes_be = x_bytes_le;
184            x_bytes_be.reverse();
185            let computed_point = p256::AffinePoint::decompress(
186                x_bytes_be.as_slice().into(),
187                Choice::from(sign_bit as u8),
188            );
189            if computed_point.is_none().into() {
190                return Err(ExitCode::MalformedBuiltinParams);
191            }
192            let point = computed_point.unwrap().to_encoded_point(false);
193            let x = BigUint::from_bytes_be(point.x().unwrap());
194            let y = BigUint::from_bytes_be(point.y().unwrap());
195            AffinePoint::<E>::new(x, y)
196        }
197        CurveType::Bls12381 => {
198            let mut g1_bytes_be = x_bytes_le;
199            g1_bytes_be.reverse();
200
201            const COMPRESSION_FLAG: u8 = 0b_1000_0000;
202            const Y_IS_ODD_FLAG: u8 = 0b_0010_0000;
203
204            let mut flags = COMPRESSION_FLAG;
205            if sign_bit == 1 {
206                flags |= Y_IS_ODD_FLAG;
207            };
208
209            // set sign and compression flag
210            g1_bytes_be[0] |= flags;
211            let point =
212                deserialize_g1(&g1_bytes_be).map_err(|_| ExitCode::MalformedBuiltinParams)?;
213
214            // TODO(dmitry123): These unwraps won't fire, but it's better to operate with raw bytes,
215            //  instead of hex encoding/decoding. This code is copied from SP1...
216            let x_str = point.getx().to_string();
217            let x = BigUint::from_str_radix(x_str.as_str(), 16).unwrap();
218            let y_str = point.gety().to_string();
219            let y = BigUint::from_str_radix(y_str.as_str(), 16).unwrap();
220
221            AffinePoint::new(x, y)
222        }
223        _ => panic!("unsupported curve: {}", E::CURVE_TYPE),
224    };
225
226    let y_bytes_le = computed_point.y.to_bytes_le();
227
228    let mut result_bytes_le = [0u8; DECOMPRESSED_SIZE];
229    let (result_y, result_x) = result_bytes_le.split_at_mut(DECOMPRESSED_SIZE / 2);
230    result_y[..y_bytes_le.len()].clone_from_slice(&y_bytes_le);
231    result_x[..x_bytes_le.len()].clone_from_slice(&x_bytes_le);
232    Ok(result_bytes_le)
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use amcl::{
239        bls381::bls381::{basic::key_pair_generate_g2, utils::deserialize_g1},
240        rand::RAND,
241    };
242    use fluentbase_types::hex;
243    use rand::{rng, Rng, RngCore};
244
245    pub fn p256_decompress(compressed_key: &[u8]) -> [u8; SECP256R1_G1_RAW_AFFINE_SIZE] {
246        assert_eq!(compressed_key.len(), 33);
247        let is_odd = match compressed_key[0] {
248            2 => false,
249            3 => true,
250            _ => unreachable!(),
251        };
252        let mut compressed_key: [u8; 32] = compressed_key[1..].try_into().unwrap();
253        compressed_key.reverse();
254        let mut result: [u8; 64] =
255            syscall_secp256r1_decompress_impl(compressed_key, is_odd as u32).unwrap();
256        result.reverse();
257        result
258    }
259
260    /// This test is reconstructed from SP1 sources:
261    /// - sp1/crates/test-artifacts/programs/secp256r1-decompress/src/main.rs
262    /// - sp1/crates/core/machine/src/syscall/precompiles/weierstrass/weierstrass_decompress.rs
263    /// - sp1/crates/zkvm/entrypoint/src/syscalls/secp256r1.rs
264    #[test]
265    fn test_secp256r1_sp1_decompress() {
266        let mut rng = rng();
267        for _ in 0..100 {
268            let mut random_private_key = [0u8; 32];
269            rng.fill_bytes(&mut random_private_key);
270            let secret_key = p256::SecretKey::from_slice(&random_private_key).unwrap();
271            let public_key = secret_key.public_key();
272            let decompressed = public_key.to_encoded_point(false).to_bytes();
273            let compressed = public_key.to_encoded_point(true).to_bytes();
274            let result = p256_decompress(&compressed);
275            assert_eq!(hex::encode(result), hex::encode(&decompressed[1..]));
276        }
277    }
278
279    #[test]
280    #[should_panic(
281        expected = "called `Result::unwrap()` on an `Err` value: MalformedBuiltinParams"
282    )]
283    fn test_secp256r1_bad_point() {
284        let mut point: [u8; 33] = [0xff; 33];
285        point[0] = 0x03;
286        let _ = p256_decompress(&point);
287    }
288
289    pub fn k256_decompress(compressed_key: &[u8]) -> [u8; SECP256K1_G1_RAW_AFFINE_SIZE] {
290        assert_eq!(compressed_key.len(), 33);
291        let is_odd = match compressed_key[0] {
292            2 => false,
293            3 => true,
294            _ => panic!("Invalid compressed key"),
295        };
296        let mut compressed_key: [u8; 32] = compressed_key[1..].try_into().unwrap();
297        compressed_key.reverse();
298        let mut result: [u8; 64] =
299            syscall_secp256k1_decompress_impl(compressed_key, is_odd as u32).unwrap();
300        result.reverse();
301        result
302    }
303
304    #[test]
305    fn test_secp256k1_sp1_decompress() {
306        let mut rng = rng();
307        for _ in 0..100 {
308            let mut random_private_key = [0u8; 32];
309            rng.fill_bytes(&mut random_private_key);
310            let secret_key = k256::SecretKey::from_slice(&random_private_key).unwrap();
311            let public_key = secret_key.public_key();
312            let decompressed = public_key.to_encoded_point(false).to_bytes();
313            let compressed = public_key.to_encoded_point(true).to_bytes();
314            let result = k256_decompress(&compressed);
315            assert_eq!(hex::encode(result), hex::encode(&decompressed[1..]));
316        }
317    }
318
319    #[test]
320    #[should_panic(
321        expected = "called `Result::unwrap()` on an `Err` value: MalformedBuiltinParams"
322    )]
323    fn test_secp256k1_bad_point() {
324        let mut point: [u8; 33] = [0xff; 33];
325        point[0] = 0x03;
326        let _ = k256_decompress(&point);
327    }
328
329    pub fn bls12381_decompress(
330        compressed_key: [u8; BLS12381_G1_COMPRESSED_SIZE],
331    ) -> [u8; BLS12381_G1_RAW_AFFINE_SIZE] {
332        let mut compressed_key_unsigned = [0u8; BLS12381_G1_COMPRESSED_SIZE];
333        compressed_key_unsigned.copy_from_slice(&compressed_key);
334        let sign_bit = ((compressed_key_unsigned[0] & 0b_0010_0000) >> 5) == 1;
335        compressed_key_unsigned[0] &= 0b_0001_1111;
336        compressed_key_unsigned.reverse();
337        let mut result: [u8; 96] =
338            syscall_bls12381_decompress_impl(compressed_key_unsigned, sign_bit as u32).unwrap();
339        result.reverse();
340        result
341    }
342
343    #[test]
344    fn test_bls12381_sp1_decompress() {
345        let mut rng = rng();
346        let mut rand = RAND::new();
347        let len = 100;
348        let num_tests = 10;
349        let random_slice = (0..len).map(|_| rng.random::<u8>()).collect::<Vec<u8>>();
350        rand.seed(len, &random_slice);
351        for _ in 0..num_tests {
352            let (_, compressed) = key_pair_generate_g2(&mut rand);
353            let point = deserialize_g1(&compressed).unwrap();
354            let x = point.getx().to_string();
355            let y = point.gety().to_string();
356            let result = bls12381_decompress(compressed);
357            assert_eq!(
358                hex::encode(result).to_lowercase(),
359                format!("{x}{y}").to_lowercase()
360            );
361        }
362    }
363
364    #[test]
365    #[should_panic(
366        expected = "called `Result::unwrap()` on an `Err` value: MalformedBuiltinParams"
367    )]
368    fn test_bls12381_bad_point() {
369        let point: [u8; 48] = [0xff; 48];
370        let _ = bls12381_decompress(point);
371    }
372}