diff --git a/Cargo.lock b/Cargo.lock index c0b55ae35..7bd0916ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -906,7 +906,9 @@ version = "0.1.0" dependencies = [ "alloy-consensus", "alloy-primitives", + "ceno_keccak", "ceno_rt", + "k256", "revm-precompile", "thiserror 2.0.12", ] @@ -959,6 +961,7 @@ name = "ceno_rt" version = "0.1.0" dependencies = [ "getrandom 0.2.16", + "getrandom 0.3.2", "rkyv", ] @@ -1776,8 +1779,10 @@ name = "examples" version = "0.1.0" dependencies = [ "alloy-primitives", + "ceno_crypto", "ceno_keccak", "ceno_rt", + "getrandom 0.3.2", "rand 0.8.5", "rkyv", "substrate-bn", @@ -1825,7 +1830,7 @@ dependencies = [ [[package]] name = "ff_ext" version = "0.1.0" -source = "git+https://github.com/scroll-tech/gkr-backend.git?branch=updates-for-precompiles#0f8ab8141aadd78c69a0a67ab6bd49399563e6b9" +source = "git+https://github.com/scroll-tech/gkr-backend.git?rev=v1.0.0-alpha.9#44e4aa4456b084481a9aef1b7ee5f829221d5a0d" dependencies = [ "once_cell", "p3", @@ -2614,7 +2619,7 @@ dependencies = [ [[package]] name = "mpcs" version = "0.1.0" -source = "git+https://github.com/scroll-tech/gkr-backend.git?branch=updates-for-precompiles#0f8ab8141aadd78c69a0a67ab6bd49399563e6b9" +source = "git+https://github.com/scroll-tech/gkr-backend.git?rev=v1.0.0-alpha.9#44e4aa4456b084481a9aef1b7ee5f829221d5a0d" dependencies = [ "bincode", "clap", @@ -2638,7 +2643,7 @@ dependencies = [ [[package]] name = "multilinear_extensions" version = "0.1.0" -source = "git+https://github.com/scroll-tech/gkr-backend.git?branch=updates-for-precompiles#0f8ab8141aadd78c69a0a67ab6bd49399563e6b9" +source = "git+https://github.com/scroll-tech/gkr-backend.git?rev=v1.0.0-alpha.9#44e4aa4456b084481a9aef1b7ee5f829221d5a0d" dependencies = [ "either", "ff_ext", @@ -2959,7 +2964,7 @@ dependencies = [ [[package]] name = "p3" version = "0.1.0" -source = "git+https://github.com/scroll-tech/gkr-backend.git?branch=updates-for-precompiles#0f8ab8141aadd78c69a0a67ab6bd49399563e6b9" +source = "git+https://github.com/scroll-tech/gkr-backend.git?rev=v1.0.0-alpha.9#44e4aa4456b084481a9aef1b7ee5f829221d5a0d" dependencies = [ "p3-baby-bear", "p3-challenger", @@ -3368,7 +3373,7 @@ dependencies = [ [[package]] name = "poseidon" version = "0.1.0" -source = "git+https://github.com/scroll-tech/gkr-backend.git?branch=updates-for-precompiles#0f8ab8141aadd78c69a0a67ab6bd49399563e6b9" +source = "git+https://github.com/scroll-tech/gkr-backend.git?rev=v1.0.0-alpha.9#44e4aa4456b084481a9aef1b7ee5f829221d5a0d" dependencies = [ "ff_ext", "p3", @@ -4308,7 +4313,7 @@ dependencies = [ [[package]] name = "sp1-curves" version = "0.1.0" -source = "git+https://github.com/scroll-tech/gkr-backend.git?branch=updates-for-precompiles#0f8ab8141aadd78c69a0a67ab6bd49399563e6b9" +source = "git+https://github.com/scroll-tech/gkr-backend.git?rev=v1.0.0-alpha.9#44e4aa4456b084481a9aef1b7ee5f829221d5a0d" dependencies = [ "cfg-if", "dashu", @@ -4414,7 +4419,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sumcheck" version = "0.1.0" -source = "git+https://github.com/scroll-tech/gkr-backend.git?branch=updates-for-precompiles#0f8ab8141aadd78c69a0a67ab6bd49399563e6b9" +source = "git+https://github.com/scroll-tech/gkr-backend.git?rev=v1.0.0-alpha.9#44e4aa4456b084481a9aef1b7ee5f829221d5a0d" dependencies = [ "either", "ff_ext", @@ -4432,7 +4437,7 @@ dependencies = [ [[package]] name = "sumcheck_macro" version = "0.1.0" -source = "git+https://github.com/scroll-tech/gkr-backend.git?branch=updates-for-precompiles#0f8ab8141aadd78c69a0a67ab6bd49399563e6b9" +source = "git+https://github.com/scroll-tech/gkr-backend.git?rev=v1.0.0-alpha.9#44e4aa4456b084481a9aef1b7ee5f829221d5a0d" dependencies = [ "itertools 0.13.0", "p3", @@ -4827,7 +4832,7 @@ dependencies = [ [[package]] name = "transcript" version = "0.1.0" -source = "git+https://github.com/scroll-tech/gkr-backend.git?branch=updates-for-precompiles#0f8ab8141aadd78c69a0a67ab6bd49399563e6b9" +source = "git+https://github.com/scroll-tech/gkr-backend.git?rev=v1.0.0-alpha.9#44e4aa4456b084481a9aef1b7ee5f829221d5a0d" dependencies = [ "ff_ext", "itertools 0.13.0", @@ -5099,7 +5104,7 @@ dependencies = [ [[package]] name = "whir" version = "0.1.0" -source = "git+https://github.com/scroll-tech/gkr-backend.git?branch=updates-for-precompiles#0f8ab8141aadd78c69a0a67ab6bd49399563e6b9" +source = "git+https://github.com/scroll-tech/gkr-backend.git?rev=v1.0.0-alpha.9#44e4aa4456b084481a9aef1b7ee5f829221d5a0d" dependencies = [ "bincode", "clap", @@ -5386,7 +5391,7 @@ dependencies = [ [[package]] name = "witness" version = "0.1.0" -source = "git+https://github.com/scroll-tech/gkr-backend.git?branch=updates-for-precompiles#0f8ab8141aadd78c69a0a67ab6bd49399563e6b9" +source = "git+https://github.com/scroll-tech/gkr-backend.git?rev=v1.0.0-alpha.9#44e4aa4456b084481a9aef1b7ee5f829221d5a0d" dependencies = [ "ff_ext", "multilinear_extensions", diff --git a/ceno_host/tests/test_elf.rs b/ceno_host/tests/test_elf.rs index e7a6c3bda..35dadf6a6 100644 --- a/ceno_host/tests/test_elf.rs +++ b/ceno_host/tests/test_elf.rs @@ -438,6 +438,18 @@ fn test_secp256k1_decompress() -> Result<()> { Ok(()) } +#[test] +fn test_secp256k1_ecrecover() -> Result<()> { + let _ = ceno_host::run( + CENO_PLATFORM, + ceno_examples::secp256k1_ecrecover, + &CenoStdin::default(), + None, + ); + + Ok(()) +} + #[test] fn test_sha256_extend() -> Result<()> { let program_elf = ceno_examples::sha_extend_syscall; diff --git a/ceno_rt/Cargo.toml b/ceno_rt/Cargo.toml index fd47edc50..7e2a4e39a 100644 --- a/ceno_rt/Cargo.toml +++ b/ceno_rt/Cargo.toml @@ -11,4 +11,5 @@ version = "0.1.0" [dependencies] getrandom = { version = "0.2.15", features = ["custom"], default-features = false } +getrandom_v3 = { package = "getrandom", version = "0.3", default-features = false } rkyv.workspace = true diff --git a/ceno_rt/src/lib.rs b/ceno_rt/src/lib.rs index 1ced41d48..cc0deb217 100644 --- a/ceno_rt/src/lib.rs +++ b/ceno_rt/src/lib.rs @@ -86,6 +86,23 @@ pub fn my_get_random(buf: &mut [u8]) -> Result<(), Error> { } register_custom_getrandom!(my_get_random); +/// Custom getrandom implementation for getrandom v0.3 +/// +/// see also: +/// +/// # Safety +/// - `dest` must be valid for writes of `len` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "Rust" fn __getrandom_v03_custom( + dest: *mut u8, + len: usize, +) -> Result<(), getrandom_v3::Error> { + unsafe { + sys_rand(dest, len); + } + Ok(()) +} + pub fn halt(exit_code: u32) -> ! { #[cfg(target_arch = "riscv32")] unsafe { diff --git a/ceno_rt/src/syscalls.rs b/ceno_rt/src/syscalls.rs index 743b8a77d..914fab8b7 100644 --- a/ceno_rt/src/syscalls.rs +++ b/ceno_rt/src/syscalls.rs @@ -54,9 +54,11 @@ pub fn syscall_keccak_permute(state: &mut [u64; KECCAK_STATE_WORDS]) { /// - The caller must ensure that `p` and `q` are valid points on the `secp256k1` curve, and that `p` and `q` are not equal to each other. /// - The result is stored in the first point. #[allow(unused_variables)] -pub fn syscall_secp256k1_add(p: *mut [u32; 16], q: *mut [u32; 16]) { +pub fn syscall_secp256k1_add(p: &mut [u32; 16], q: &[u32; 16]) { #[cfg(target_os = "zkvm")] unsafe { + let p = p.as_mut_ptr(); + let q = q.as_ptr(); asm!( "ecall", in("t0") SECP256K1_ADD, @@ -79,9 +81,10 @@ pub fn syscall_secp256k1_add(p: *mut [u32; 16], q: *mut [u32; 16]) { /// For example, the word `p[0]` contains the least significant `4` bytes of `X` and their significance is maintained w.r.t `p[0]` /// - The result is stored in p #[allow(unused_variables)] -pub fn syscall_secp256k1_double(p: *mut [u32; 16]) { +pub fn syscall_secp256k1_double(p: &mut [u32; 16]) { #[cfg(target_os = "zkvm")] unsafe { + let p = p.as_mut_ptr(); asm!( "ecall", in("t0") SECP256K1_DOUBLE, diff --git a/examples-builder/build.rs b/examples-builder/build.rs index 5d566ae65..754efd0f2 100644 --- a/examples-builder/build.rs +++ b/examples-builder/build.rs @@ -57,6 +57,7 @@ fn build_elfs() { } rerun_all_but_target(Path::new("../examples")); rerun_all_but_target(Path::new("../ceno_rt")); + rerun_all_but_target(Path::new("../guest_libs")); } fn main() { diff --git a/examples/.cargo/config.toml b/examples/.cargo/config.toml index 0b79e4d7a..f831dd7d5 100644 --- a/examples/.cargo/config.toml +++ b/examples/.cargo/config.toml @@ -25,5 +25,7 @@ rustflags = [ "-Zlocation-detail=none", "-C", "passes=lower-atomic", + '--cfg', + 'getrandom_backend="custom"', # getrandom v3.3+ requires this cfg to use a custom getrandom implementation ] target = "../ceno_rt/riscv32im-ceno-zkvm-elf.json" diff --git a/examples/Cargo.toml b/examples/Cargo.toml index d9947337b..6a6bfbec0 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -11,8 +11,10 @@ version = "0.1.0" [dependencies] alloy-primitives = { version = "1.3", features = ["native-keccak"] } +ceno_crypto = { path = "../guest_libs/crypto" } ceno_keccak = { path = "../guest_libs/keccak" } ceno_rt = { path = "../ceno_rt" } +getrandom = { version = "0.3" } rand.workspace = true tiny-keccak.workspace = true diff --git a/examples/examples/secp256k1_add_syscall.rs b/examples/examples/secp256k1_add_syscall.rs index 07bb054a1..5c66cd3fa 100644 --- a/examples/examples/secp256k1_add_syscall.rs +++ b/examples/examples/secp256k1_add_syscall.rs @@ -41,9 +41,9 @@ fn bytes_to_words(bytes: [u8; 65]) -> [u32; 16] { } fn main() { let mut p: DecompressedPoint = bytes_to_words(P); - let mut q: DecompressedPoint = bytes_to_words(Q); + let q: DecompressedPoint = bytes_to_words(Q); let p_plus_q: DecompressedPoint = bytes_to_words(P_PLUS_Q); - syscall_secp256k1_add(&mut p, &mut q); + syscall_secp256k1_add(&mut p, &q); assert_eq!(p, p_plus_q); } diff --git a/examples/examples/secp256k1_ecrecover.rs b/examples/examples/secp256k1_ecrecover.rs new file mode 100644 index 000000000..5812a88f8 --- /dev/null +++ b/examples/examples/secp256k1_ecrecover.rs @@ -0,0 +1,56 @@ +// Test ecrecover of real world signatures from scroll mainnet. Assert result inside the guest. +extern crate ceno_rt; + +use alloy_primitives::{Address, B256, address, b256, hex}; +use ceno_crypto::secp256k1::secp256k1_ecrecover; + +const TEST_CASES: [(&[u8], u8, B256, Address); 5] = [ + // (sig, recid, tx_hash, signer) + ( + &hex!( + "15a7bb615483f66a697431cd414294b6bd1e1b9b9d6d163cfd97290ea77b53061810c4d228e424087ad77ee75bb25e77c832ad9038b89f7e573a34b574648348" + ), + 0, + b256!("b329f831352e37f4426583986465b065d9c867901b42f576f00ef36dfac1cfdf"), + address!("ca585e09df67e83106c9bcd839c989ace537bf95"), + ), + ( + &hex!( + "870077f742ca34760810033caf13c99e90e207db6f820124b827907e9658d7d04f302d6675c8625c02fc95c131a3ce77e7f90dba10dbda368efeaaba9be60916" + ), + 0, + b256!("4e13990772a9454712c7560ad8a64b845fd472b913b90d680867ab3dad56a18d"), + address!("a79c12bcf11133af01b6b20f16f8aafaecdebc93"), + ), + ( + &hex!( + "455a6249244154e8f5d516a3036e26576449bef05171657dbf3a5d7b9c02fe96629f7eb0aa2a006ff4ac6fc0523a6f5a365cf375240f5a560b1972eb21cec087" + ), + 1, + b256!("4dedbd995fc79db979c6484132568fe30fdf6bfa8b64ac74ba844cc30e764b0c"), + address!("c623f214c8eefc771147c5806be250db39555555"), + ), + ( + &hex!( + "854c4656c421158b4e5d8c29ccc3adcaee329587cee630398f3ce2e32745e45b67b1fc40e3206c70a75bcdf3c877c26874c75c2fabd5566c85b58c7c7d872e00" + ), + 0, + b256!("e4559e37c72fb3df0349df42b3aa0e94607287ecb3e6530b7c50ed984e0428a2"), + address!("b82def35c814584d3d929cfb3a1fb1b886b6e57b"), + ), + ( + &hex!( + "004a0ac1306d096c06fb77f82b76f43fb2459638826f4846444686b3036b9a4b3d6bf124bf22f23b851adfa2c4bdc670b4ecb5129186a4e89032916a77a56b90" + ), + 0, + b256!("83e5e11daa2d14736ab1d578c41250c6f6445782c215684a18f67b44686ccb90"), + address!("0a6f0ed4896be1caa9e37047578e7519481f22ea"), + ), +]; + +fn main() { + for (sig, recid, tx_hash, signer) in TEST_CASES { + let recovered = secp256k1_ecrecover(sig.try_into().unwrap(), recid, &tx_hash.0).unwrap(); + assert_eq!(&recovered[12..], &signer.0); + } +} diff --git a/examples/examples/syscalls.rs b/examples/examples/syscalls.rs index d5e6a486c..9318a863b 100644 --- a/examples/examples/syscalls.rs +++ b/examples/examples/syscalls.rs @@ -51,9 +51,9 @@ pub fn test_syscalls() { ]; { let mut p = bytes_to_words(P); - let mut q = bytes_to_words(Q); + let q = bytes_to_words(Q); let p_plus_q = bytes_to_words(P_PLUS_Q); - syscall_secp256k1_add(&mut p, &mut q); + syscall_secp256k1_add(&mut p, &q); assert!(p == p_plus_q); } diff --git a/guest_libs/crypto/Cargo.toml b/guest_libs/crypto/Cargo.toml index 54ef0295d..1376ab07b 100644 --- a/guest_libs/crypto/Cargo.toml +++ b/guest_libs/crypto/Cargo.toml @@ -10,7 +10,9 @@ repository = "https://github.com/scroll-tech/ceno" version = "0.1.0" [dependencies] +ceno_keccak = { path = "../keccak" } ceno_rt = { path = "../../ceno_rt" } +k256 = { version = "0.13", default-features = false, features = ["std", "ecdsa"] } thiserror.workspace = true [dev-dependencies] diff --git a/guest_libs/crypto/src/lib.rs b/guest_libs/crypto/src/lib.rs index 0d52933aa..004e2542c 100644 --- a/guest_libs/crypto/src/lib.rs +++ b/guest_libs/crypto/src/lib.rs @@ -34,7 +34,7 @@ pub enum CenoCryptoError { Bn254PairLength, /// Sepk256k1 ecrecover error #[error("Secp256k1 ecrecover error")] - Secp256k1EcRecover, + Secp256k1Ecrecover(#[from] k256::ecdsa::Error), } #[cfg(test)] diff --git a/guest_libs/crypto/src/secp256k1.rs b/guest_libs/crypto/src/secp256k1.rs index a4ca088c6..93bdf63f3 100644 --- a/guest_libs/crypto/src/secp256k1.rs +++ b/guest_libs/crypto/src/secp256k1.rs @@ -1,11 +1,211 @@ use crate::CenoCryptoError; +use ceno_keccak::{Hasher, Keccak}; +use ceno_rt::syscalls::{syscall_secp256k1_add, syscall_secp256k1_double}; +use k256::{ + AffinePoint, EncodedPoint, FieldBytes, NonZeroScalar, Scalar, Secp256k1, U256, + ecdsa::{Error, RecoveryId, Signature, hazmat::bits2field}, + elliptic_curve::{ + Curve, Field, FieldBytesEncoding, PrimeField, + bigint::CheckedAdd, + ops::{Invert, Reduce}, + sec1::{FromEncodedPoint, ToEncodedPoint}, + }, +}; + +type UntaggedUncompressedPoint = [u8; 64]; + +#[repr(align(4))] +struct Aligned64([u8; 64]); /// secp256k1 ECDSA signature recovery. #[inline] pub fn secp256k1_ecrecover( - _sig: &[u8; 64], - _recid: u8, - _msg: &[u8; 32], + sig: &[u8; 64], + recid: u8, + msg: &[u8; 32], ) -> Result<[u8; 32], CenoCryptoError> { - unimplemented!() + // Copied from + let mut signature = Signature::from_slice(sig)?; + let mut recid = recid; + + // normalize signature and flip recovery id if needed. + if let Some(sig_normalized) = signature.normalize_s() { + signature = sig_normalized; + recid ^= 1; + } + let recid = RecoveryId::from_byte(recid).expect("recovery ID is valid"); + + // recover key + let recovered_key = recover_from_prehash(&msg[..], &signature, recid)?; + + let mut hasher = Keccak::v256(); + hasher.update(&recovered_key); + let mut hash = [0u8; 32]; + hasher.finalize(&mut hash); + // truncate to 20 bytes + hash[..12].fill(0); + Ok(hash) +} + +/// Copied from +/// Modified to use ceno syscalls +fn recover_from_prehash( + prehash: &[u8], + signature: &Signature, + recovery_id: RecoveryId, +) -> Result { + let (r, s) = signature.split_scalars(); + let prehash: FieldBytes = bits2field::(prehash)?; + let z = >::reduce_bytes(&prehash); + + let mut r_bytes = r.to_repr(); + if recovery_id.is_x_reduced() { + let decoded: U256 = FieldBytesEncoding::::decode_field_bytes(&r_bytes); + match decoded.checked_add(&Secp256k1::ORDER).into_option() { + Some(restored) => { + r_bytes = >::encode_field_bytes(&restored) + } + // No reduction should happen here if r was reduced + None => return Err(Error::new()), + }; + } + + // Modified part: use ceno syscall to decompress point + // Original: + // let R = AffinePoint::decompress(&r_bytes, u8::from(recovery_id.is_y_odd()).into()); + let r_point = { + let mut buf = Aligned64([0u8; 64]); + buf.0[..32].copy_from_slice(&r_bytes); + + // SAFETY: + // [x] The input array should be 64 bytes long, with the first 32 bytes containing the X coordinate in + // big-endian format. + // [x] The caller must ensure that `point` is valid pointer to data that is aligned along a four byte + // boundary. + ceno_rt::syscalls::syscall_secp256k1_decompress(&mut buf.0, recovery_id.is_y_odd()); + let point = EncodedPoint::from_untagged_bytes((&buf.0).into()); + AffinePoint::from_encoded_point(&point) + }; + + let Some(r_point) = r_point.into_option() else { + return Err(Error::new()); + }; + + // TODO: scalar syscalls + let r_inv = *r.invert(); + let u1 = -(r_inv * z); + let u2 = r_inv * *s; + + // Original: + // ProjectivePoint::lincomb(&ProjectivePoint::GENERATOR, &u1, &r_point, &u2) + // Equivalent to: G * u1 + R * u2 + let pk_words = lincomb(u1, &r_point, u2)?; + + let bytes = words_to_untagged_bytes(pk_words); + + // Original: + // let vk = VerifyingKey::from_affine(pk.to_affine())?; + // // Ensure signature verifies with the recovered key + // vk.verify_prehash(prehash, signature)?; + verify_prehash(&z, (&r, &s), &bytes)?; + + Ok(bytes) +} + +/// Copied from +fn verify_prehash( + z: &Scalar, + (r, s): (&NonZeroScalar, &NonZeroScalar), + bytes: &UntaggedUncompressedPoint, +) -> Result<(), Error> { + let q = EncodedPoint::from_untagged_bytes(bytes.into()); + let q = AffinePoint::from_encoded_point(&q) + .into_option() + .ok_or(Error::new())?; + + let s_inv = *s.invert_vartime(); + let u1 = z * &s_inv; + let u2 = **r * s_inv; + + // Original: + // let x = ProjectivePoint::lincomb(&ProjectivePoint::GENERATOR, &u1, &q, &u2) + // .to_affine() + // .x(); + // Equivalent to: G * u1 + q * u2 + let p = lincomb(u1, &q, u2)?; + let p_bytes = words_to_untagged_bytes(p); + let x = FieldBytes::from_slice(&p_bytes[..32]); + + if **r == >::reduce_bytes(x) { + Ok(()) + } else { + Err(Error::new()) + } +} + +#[inline] +fn lincomb(u1: Scalar, p: &AffinePoint, u2: Scalar) -> Result<[u32; 16], Error> { + Ok(match (u1 == Scalar::ZERO, u2 == Scalar::ZERO) { + (false, false) => { + let mut p1 = secp256k1_mul(&AffinePoint::GENERATOR, u1); + let p2 = secp256k1_mul(p, u2); + syscall_secp256k1_add(&mut p1, &p2); + p1 + } + (true, false) => secp256k1_mul(p, u2), + (false, true) => secp256k1_mul(&AffinePoint::GENERATOR, u1), + (true, true) => return Err(Error::new()), + }) +} + +fn secp256k1_mul(point: &AffinePoint, scalar: Scalar) -> [u32; 16] { + let mut base = point_to_words(point.to_encoded_point(false)); + let mut acc: [u32; 16] = [0; 16]; + let mut acc_init = false; + + let mut k = scalar; + while !k.is_zero_vartime() { + if bool::from(k.is_odd()) { + if !acc_init { + acc = base; + acc_init = true; + } else { + // SAFETY: syscall requires point not to be infinity + let tmp = base; + syscall_secp256k1_add(&mut acc, &tmp) + } + } + syscall_secp256k1_double(&mut base); + k = k.shr_vartime(1); + } + + acc +} + +/// `bytes` is expected to contain the uncompressed representation of +/// a curve point, as described in https://docs.rs/secp/latest/secp/struct.Point.html +/// +/// The return value is an array of words compatible with the sp1 syscall for `add` and `double` +/// Notably, these words should encode the X and Y coordinates of the point +/// in "little endian" and not "big endian" as is the case of secp +fn point_to_words(point: EncodedPoint) -> [u32; 16] { + debug_assert!(!point.is_compressed()); + // ignore the tag byte (specific to the secp repr.) + let mut bytes: [u8; 64] = point.as_bytes()[1..].try_into().unwrap(); + + // Reverse the order of bytes for each coordinate + bytes[0..32].reverse(); + bytes[32..].reverse(); + std::array::from_fn(|i| u32::from_le_bytes(bytes[4 * i..4 * (i + 1)].try_into().unwrap())) +} + +fn words_to_untagged_bytes(words: [u32; 16]) -> UntaggedUncompressedPoint { + let mut bytes = [0u8; 64]; + for i in 0..16 { + bytes[4 * i..4 * (i + 1)].copy_from_slice(&words[i].to_le_bytes()); + } + // Reverse the order of bytes for each coordinate + bytes[..32].reverse(); + bytes[32..].reverse(); + bytes }