From ed4a212bcb2c9574682fb034e5424b90aa76c51c Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Tue, 19 May 2026 08:56:06 +0900 Subject: [PATCH] [solana-bn254-syscall] Add `solana-bn254-syscall` crate (#13) * add `solana-bn254-syscall` crate * use `Validate::Yes` for `G1::deserialize_with_mode` * fix crate name * inherit dependencies from workspace * update edition to 2021 * remove unnecessary `is_on_curve` check * add `#[inline(always)]` * make `PodG1` and `PodG2` pub(crate) * remove unnecessary `is_on_curve` check * remove custom logic for legacy versions * `convert_endianness` -> `swap_endianness` * simplify `swap_endianness` * update function return types to arrays instead of vecs * copy over docs * update cargo lock * copy over unit tests for serialization * clean up docs for the syscall implementation functions * Apply suggestions from code review Co-authored-by: Stanislav Ladyzhenskiy * Update syscall/bn254-syscall/src/multiplication.rs Co-authored-by: Stanislav Ladyzhenskiy * cargo lock * remove `all-features` and `rustdoc-args` * Apply suggestions from code review Co-authored-by: Stanislav Ladyzhenskiy * Update syscall/bn254-syscall/Cargo.toml Co-authored-by: Stanislav Ladyzhenskiy * remove `include = ...` --------- Co-authored-by: Stanislav Ladyzhenskiy --- Cargo.lock | 212 +++++++++++++++++- Cargo.toml | 5 + syscall/bn254-syscall/Cargo.toml | 20 ++ syscall/bn254-syscall/src/addition.rs | 143 +++++++++++++ syscall/bn254-syscall/src/lib.rs | 225 ++++++++++++++++++++ syscall/bn254-syscall/src/multiplication.rs | 159 ++++++++++++++ syscall/bn254-syscall/src/pairing.rs | 86 ++++++++ 7 files changed, 848 insertions(+), 2 deletions(-) create mode 100644 syscall/bn254-syscall/Cargo.toml create mode 100644 syscall/bn254-syscall/src/addition.rs create mode 100644 syscall/bn254-syscall/src/lib.rs create mode 100644 syscall/bn254-syscall/src/multiplication.rs create mode 100644 syscall/bn254-syscall/src/pairing.rs diff --git a/Cargo.lock b/Cargo.lock index 0af491a..2fabdc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -35,6 +47,136 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools 0.13.0", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.5.0" @@ -152,6 +294,9 @@ name = "bytemuck" version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] [[package]] name = "bytemuck_derive" @@ -482,12 +627,44 @@ dependencies = [ "zeroize", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -742,6 +919,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1232,7 +1418,7 @@ dependencies = [ "bitflags", "num-traits", "rand 0.9.4", - "rand_chacha", + "rand_chacha 0.9.0", "rand_xorshift 0.4.0", "regex-syntax", "rusty-fork", @@ -1279,6 +1465,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ + "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -1288,7 +1475,7 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.5", ] @@ -1303,6 +1490,16 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -1572,6 +1769,17 @@ dependencies = [ "pairing", ] +[[package]] +name = "solana-bn254-syscall" +version = "0.1.0" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-serialize", + "bytemuck", +] + [[package]] name = "solana-curve25519-cuda" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index aa2d396..b02bf6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "curve25519/solana-ed25519", "experimental/ed25519-pokos", "syscall/bls12-381-syscall", + "syscall/bn254-syscall", ] resolver = "2" @@ -15,6 +16,10 @@ license = "Apache-2.0" rust-version = "1.89.0" # solana platform-tools rust version [workspace.dependencies] +ark-bn254 = "0.5.0" +ark-ec = "0.5.0" +ark-ff = "0.5.0" +ark-serialize = "0.5.0" bincode = "1" blst = "0.3.16" blstrs = "0.7.1" diff --git a/syscall/bn254-syscall/Cargo.toml b/syscall/bn254-syscall/Cargo.toml new file mode 100644 index 0000000..345a214 --- /dev/null +++ b/syscall/bn254-syscall/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "solana-bn254-syscall" +description = "Solana BN254 Syscall" +documentation = "https://docs.rs/solana-bn254-syscall" +version = "0.1.0" +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = "2021" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +ark-bn254 = { workspace = true } +ark-ec = { workspace = true } +ark-ff = { workspace = true } +ark-serialize = { workspace = true } +bytemuck = { workspace = true, features = ["derive"] } diff --git a/syscall/bn254-syscall/src/addition.rs b/syscall/bn254-syscall/src/addition.rs new file mode 100644 index 0000000..6f3a8eb --- /dev/null +++ b/syscall/bn254-syscall/src/addition.rs @@ -0,0 +1,143 @@ +use { + crate::{ + swap_endianness, Endianness, PodG1, PodG2, ALT_BN128_FIELD_SIZE, ALT_BN128_FQ2_SIZE, + ALT_BN128_G1_POINT_SIZE, ALT_BN128_G2_POINT_SIZE, G1, G2, + }, + ark_serialize::{CanonicalSerialize, Compress}, +}; + +/// Input size for the g1 add operation. +pub const ALT_BN128_G1_ADDITION_INPUT_SIZE: usize = ALT_BN128_G1_POINT_SIZE * 2; // 128 + +/// Input size for the g2 add operation. +pub const ALT_BN128_G2_ADDITION_INPUT_SIZE: usize = ALT_BN128_G2_POINT_SIZE * 2; // 256 + +/// The enum is used to version changes to the `alt_bn128_versioned_g1_addition` function. +pub enum VersionedG1Addition { + V0, +} + +/// The enum is used to version changes to the `alt_bn128_versioned_g2_addition` function. +pub enum VersionedG2Addition { + V0, +} + +/// The implementation of the `sol_alt_bn128_group_op` syscall G1 addition operation +/// (group operation index 0x00 for BE input/output, 0x80 for LE input/output). +/// +/// **Security Note** +/// +/// Because the BN254 G1 group has a cofactor of 1, the subgroup check is equivalent +/// to verifying the point is on the curve. This function fully validates the input point. +/// +/// **Warning** +/// +/// This is consensus-critical Agave validator code. Modifying this +/// function can result in a network fork. See the [crate-level documentation](crate) +/// for strict guidelines on SIMD approvals and versioning. +pub fn alt_bn128_versioned_g1_addition( + _version: VersionedG1Addition, + input: &[u8], + endianness: Endianness, +) -> Option<[u8; ALT_BN128_G1_POINT_SIZE]> { + let is_valid_len = match endianness { + Endianness::BE => input.len() <= ALT_BN128_G1_ADDITION_INPUT_SIZE, + Endianness::LE => input.len() == ALT_BN128_G1_ADDITION_INPUT_SIZE, + }; + + if !is_valid_len { + return None; + } + + let mut padded_input = [0u8; ALT_BN128_G1_ADDITION_INPUT_SIZE]; + padded_input[..input.len()].copy_from_slice(input); + + let (p_bytes, q_bytes) = padded_input.split_at(ALT_BN128_G1_POINT_SIZE); + + let (p, q) = match endianness { + Endianness::BE => ( + PodG1::from_be_bytes(p_bytes)?.into_affine()?, + PodG1::from_be_bytes(q_bytes)?.into_affine()?, + ), + Endianness::LE => ( + PodG1::from_le_bytes(p_bytes)?.into_affine()?, + PodG1::from_le_bytes(q_bytes)?.into_affine()?, + ), + }; + + let result_point_affine: G1 = (p + q).into(); + + let mut result_point_data = [0u8; ALT_BN128_G1_POINT_SIZE]; + result_point_affine + .x + .serialize_with_mode(&mut result_point_data[..ALT_BN128_FIELD_SIZE], Compress::No) + .ok()?; + result_point_affine + .y + .serialize_with_mode(&mut result_point_data[ALT_BN128_FIELD_SIZE..], Compress::No) + .ok()?; + + match endianness { + Endianness::BE => Some(swap_endianness::< + ALT_BN128_FIELD_SIZE, + ALT_BN128_G1_POINT_SIZE, + >(result_point_data)), + Endianness::LE => Some(result_point_data), + } +} + +/// The implementation of the `sol_alt_bn128_group_op` syscall G2 addition operation +/// (group operation index 0x04 for BE input/output, 0x84 for LE input/output). +/// +/// **Security Note** +/// +/// Unlike G1, which has a cofactor of 1, the group G2 has a high cofactor. +/// This G2 addition function validates only the curve equation; it does not perform +/// a subgroup (coset) check. +/// +/// **Warning** +/// +/// This is consensus-critical Agave validator code. Modifying this function can +/// result in a network fork. See the [crate-level documentation](crate) for strict +/// guidelines on SIMD approvals and versioning. +pub fn alt_bn128_versioned_g2_addition( + _version: VersionedG2Addition, + input: &[u8], + endianness: Endianness, +) -> Option<[u8; ALT_BN128_G2_POINT_SIZE]> { + if input.len() != ALT_BN128_G2_ADDITION_INPUT_SIZE { + return None; + } + + let (p_bytes, q_bytes) = input.split_at(ALT_BN128_G2_POINT_SIZE); + + let (p, q) = match endianness { + Endianness::BE => ( + PodG2::from_be_bytes(p_bytes)?.into_affine_unchecked()?, + PodG2::from_be_bytes(q_bytes)?.into_affine_unchecked()?, + ), + Endianness::LE => ( + PodG2::from_le_bytes(p_bytes)?.into_affine_unchecked()?, + PodG2::from_le_bytes(q_bytes)?.into_affine_unchecked()?, + ), + }; + + let result_point_affine: G2 = (p + q).into(); + + let mut result_point_data = [0u8; ALT_BN128_G2_POINT_SIZE]; + result_point_affine + .x + .serialize_with_mode(&mut result_point_data[..ALT_BN128_FQ2_SIZE], Compress::No) + .ok()?; + result_point_affine + .y + .serialize_with_mode(&mut result_point_data[ALT_BN128_FQ2_SIZE..], Compress::No) + .ok()?; + + match endianness { + Endianness::BE => { + Some(swap_endianness::(result_point_data)) + } + Endianness::LE => Some(result_point_data), + } +} diff --git a/syscall/bn254-syscall/src/lib.rs b/syscall/bn254-syscall/src/lib.rs new file mode 100644 index 0000000..e8376e0 --- /dev/null +++ b/syscall/bn254-syscall/src/lib.rs @@ -0,0 +1,225 @@ +//! # Solana BN254 Syscalls +//! +//! ** Consensus-Critical Validator Code** +//! +//! The syscall implementations in this crate are intended to be used by the +//! Agave validator client and exist primarily for validator code. Solana +//! programs or other downstream projects should use the functions from the +//! `solana-bn254` crate in the `solana-sdk` instead. +//! +//! Developers should be extremely careful when modifying these functions, as a +//! breaking change can result in a fork in the Solana cluster. Any such change +//! requires an approved Solana SIMD. Subsequently, a new version variant must +//! be added, and the new logic must be scoped to that variant. + +pub mod addition; +pub mod multiplication; +pub mod pairing; + +use { + ark_ec::AffineRepr, + ark_serialize::{CanonicalDeserialize, Compress, Validate}, + bytemuck::{Pod, Zeroable}, +}; + +/// Size of the EC point field, in bytes. +pub const ALT_BN128_FIELD_SIZE: usize = 32; + +/// Size of the extension field element (Fq2), in bytes. +pub const ALT_BN128_FQ2_SIZE: usize = ALT_BN128_FIELD_SIZE * 2; + +/// Size of the EC point. `alt_bn128` point contains +/// the consistently united x and y fields as 64 bytes. +pub const ALT_BN128_G1_POINT_SIZE: usize = ALT_BN128_FIELD_SIZE * 2; + +/// Elements in G2 is represented by 2 field-extension elements `(x, y)`. +pub const ALT_BN128_G2_POINT_SIZE: usize = ALT_BN128_FQ2_SIZE * 2; + +/// The BN254 (BN128) group element in G1 as a POD type. +/// +/// A group element in G1 consists of two field elements `(x, y)`. A `PodG1` +/// type expects a group element to be encoded as `[le(x), le(y)]` where +/// `le(..)` is the little-endian encoding of the input field element as used +/// in the `ark-bn254` crate. Note that this differs from the EIP-197 standard, +/// which specifies that the field elements are encoded as big-endian. +/// +/// `PodG1` can be constructed from both big-endian (EIP-197) and little-endian +/// (ark-bn254) encodings using `from_be_bytes` and `from_le_bytes` methods, +/// respectively. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable)] +#[repr(transparent)] +pub(crate) struct PodG1(pub [u8; ALT_BN128_G1_POINT_SIZE]); + +/// The BN254 (BN128) group element in G2 as a POD type. +/// +/// Elements in G2 is represented by 2 field-extension elements `(x, y)`. Each +/// field-extension element itself is a degree 1 polynomial `x = x0 + x1*X`, +/// `y = y0 + y1*X`. The EIP-197 standard encodes a G2 element as +/// `[be(x1), be(x0), be(y1), be(y0)]` where `be(..)` is the big-endian +/// encoding of the input field element. The `ark-bn254` crate encodes a G2 +/// element as `[le(x0), le(x1), le(y0), le(y1)]` where `le(..)` is the +/// little-endian encoding of the input field element. Notably, in addition to +/// the differences in the big-endian vs. little-endian encodings of field +/// elements, the order of the polynomial field coefficients `x0`, `x1`, `y0`, +/// and `y1` are different. +/// +/// `PodG2` can be constructed from both big-endian (EIP-197) and little-endian +/// (ark-bn254) encodings using `from_be_bytes` and `from_le_bytes` methods, +/// respectively. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable)] +#[repr(transparent)] +pub(crate) struct PodG2(pub [u8; ALT_BN128_G2_POINT_SIZE]); + +pub(crate) type G1 = ark_bn254::g1::G1Affine; +pub(crate) type G2 = ark_bn254::g2::G2Affine; + +pub enum Endianness { + BE, + LE, +} + +/// This function swaps the endianness of each element within the input byte array. +/// It splits the input byte array of size `ARRAY_SIZE` into chunks of `CHUNK_SIZE` +/// and reverses the byte order within each chunk. +/// +/// Typical use cases: +/// - `swap_endianness::<32, 64>` for a G1 point +/// - `swap_endianness::<64, 128>` for a G2 point +/// - `swap_endianness::<32, 32>` for a scalar +pub(crate) fn swap_endianness( + mut bytes: [u8; ARRAY_SIZE], +) -> [u8; ARRAY_SIZE] { + debug_assert!( + ARRAY_SIZE.is_multiple_of(CHUNK_SIZE), + "ARRAY_SIZE must be a multiple of CHUNK_SIZE" + ); + + for chunk in bytes.chunks_exact_mut(CHUNK_SIZE) { + chunk.reverse(); + } + bytes +} + +impl PodG1 { + /// Deserializes to an affine point in G1. + /// Because G1 has a cofactor of 1, the subgroup check is equivalent to the + /// on-curve check. + pub(crate) fn into_affine(self) -> Option { + // pre-handle point at infinity + if self.0 == [0u8; 64] { + return Some(G1::zero()); + } + + // The ark-serialize uncompressed format expects 64 bytes of coordinates + // plus a 1-byte metadata flag. We append a 0 byte to indicate infinity = false. + let mut buf = [0u8; 65]; + buf[..64].copy_from_slice(&self.0); + + // Validate::Yes performs the necessary subgroup checks + G1::deserialize_with_mode(&buf[..], Compress::No, Validate::Yes).ok() + } + + /// Takes in an EIP-197 (big-endian) byte encoding of a group element in G1 and constructs a + /// `PodG1` struct that encodes the same bytes in little-endian. + #[inline(always)] + pub(crate) fn from_be_bytes(be_bytes: &[u8]) -> Option { + let pod_bytes = swap_endianness::( + be_bytes.try_into().ok()?, + ); + Some(Self(pod_bytes)) + } + + /// Takes in a little-endian byte encoding of a group element in G1 and constructs a + /// `PodG1` struct that encodes the same bytes internally. + #[inline(always)] + pub(crate) fn from_le_bytes(le_bytes: &[u8]) -> Option { + le_bytes.try_into().ok().map(Self) + } +} + +impl PodG2 { + /// Deserializes to an affine point in G2. + /// This function performs both the curve equation check and the subgroup check. + pub(crate) fn into_affine(self) -> Option { + // pre-handle point at infinity + if self.0 == [0u8; 128] { + return Some(G2::zero()); + } + + // The ark-serialize uncompressed format expects 128 bytes of coordinates + // plus a 1-byte metadata flag. We append a 0 byte to indicate infinity = false. + let mut buf = [0u8; 129]; + buf[..128].copy_from_slice(&self.0); + + // Validate::Yes performs the necessary subgroup checks + G2::deserialize_with_mode(&buf[..], Compress::No, Validate::Yes).ok() + } + + /// Deserializes to an affine point in G2. + /// This function performs the curve equation check, but skips the subgroup check. + pub(crate) fn into_affine_unchecked(self) -> Option { + // pre-handle point at infinity + if self.0 == [0u8; 128] { + return Some(G2::zero()); + } + + // The `ark-serialize` uncompressed format for affine points expects the + // x and y coordinates (128-bytes total) followed by a 1-byte metadata flag. + // We explicitly handle point at infinity above, so we append `0` to indicate + // `infinity = false`. + let mut buf = [0u8; 129]; + buf[..128].copy_from_slice(&self.0); + + // Skips the expensive subgroup check + let g2 = G2::deserialize_with_mode(&buf[..], Compress::No, Validate::No).ok()?; + + // Still check if point is on the curve + g2.is_on_curve().then_some(g2) + } + + /// Takes in an EIP-197 (big-endian) byte encoding of a group element in G2 + /// and constructs a `PodG2` struct that encodes the same bytes in + /// little-endian. + #[inline(always)] + pub(crate) fn from_be_bytes(be_bytes: &[u8]) -> Option { + let pod_bytes = swap_endianness::( + be_bytes.try_into().ok()?, + ); + Some(Self(pod_bytes)) + } + + /// Takes in a little-endian byte encoding of a group element in G2 and constructs a + /// `PodG2` struct that encodes the same bytes internally. + #[inline(always)] + pub(crate) fn from_le_bytes(le_bytes: &[u8]) -> Option { + le_bytes.try_into().ok().map(Self) + } +} + +#[cfg(test)] +mod tests { + use { + crate::PodG1, + ark_bn254::g1::G1Affine, + ark_ec::AffineRepr, + ark_serialize::{CanonicalSerialize, Compress}, + }; + + #[test] + fn zero_serialization_test() { + let zero = G1Affine::zero(); + let mut result_point_data = [0u8; 64]; + zero.x + .serialize_with_mode(&mut result_point_data[..32], Compress::No) + .unwrap(); + zero.y + .serialize_with_mode(&mut result_point_data[32..], Compress::No) + .unwrap(); + assert_eq!(result_point_data, [0u8; 64]); + + let p: G1Affine = PodG1(result_point_data[..64].try_into().unwrap()) + .into_affine() + .unwrap(); + assert_eq!(p, zero); + } +} diff --git a/syscall/bn254-syscall/src/multiplication.rs b/syscall/bn254-syscall/src/multiplication.rs new file mode 100644 index 0000000..5eae9fc --- /dev/null +++ b/syscall/bn254-syscall/src/multiplication.rs @@ -0,0 +1,159 @@ +use { + crate::{ + swap_endianness, Endianness, PodG1, PodG2, ALT_BN128_FIELD_SIZE, ALT_BN128_FQ2_SIZE, + ALT_BN128_G1_POINT_SIZE, ALT_BN128_G2_POINT_SIZE, G1, G2, + }, + ark_ec::{self, AffineRepr}, + ark_ff::BigInteger256, + ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress}, +}; + +/// Input size for the g1 multiplication operation. +pub const ALT_BN128_G1_MULTIPLICATION_INPUT_SIZE: usize = + ALT_BN128_G1_POINT_SIZE + ALT_BN128_FIELD_SIZE; // 96 + +/// Input size for the g2 multiplication operation. +pub const ALT_BN128_G2_MULTIPLICATION_INPUT_SIZE: usize = + ALT_BN128_G2_POINT_SIZE + ALT_BN128_FIELD_SIZE; // 160 + +/// The enum is used to version changes to the `alt_bn128_versioned_g1_multiplication` function. +pub enum VersionedG1Multiplication { + V0, + /// SIMD-0222 - Fix alt-bn128-multiplication Syscall Length Check + V1, +} + +/// The enum is used to version changes to the `alt_bn128_versioned_g2_multiplication` function. +pub enum VersionedG2Multiplication { + V0, +} + +/// The implementation of the `sol_alt_bn128_group_op` syscall G1 multiplication operation +/// (group operation index 0x02 for BE input/output, 0x82 for LE input/output). +/// +/// **Security Note** +/// +/// Because the BN254 G1 group has a cofactor of 1, the subgroup check is equivalent +/// to verifying the point is on the curve. This function fully validates the input point. +/// +/// **Warning** +/// +/// This is consensus-critical Agave validator code. Modifying this function can +/// result in a network fork. See the [crate-level documentation](crate) for strict +/// guidelines on SIMD approvals and versioning. +pub fn alt_bn128_versioned_g1_multiplication( + version: VersionedG1Multiplication, + input: &[u8], + endianness: Endianness, +) -> Option<[u8; ALT_BN128_G1_POINT_SIZE]> { + // reject deprecated variants + if matches!(version, VersionedG1Multiplication::V0) { + return None; + } + + let is_valid_len = match endianness { + Endianness::BE => input.len() <= ALT_BN128_G1_MULTIPLICATION_INPUT_SIZE, + Endianness::LE => input.len() == ALT_BN128_G1_MULTIPLICATION_INPUT_SIZE, + }; + + if !is_valid_len { + return None; + } + + let mut padded_input = [0u8; ALT_BN128_G1_MULTIPLICATION_INPUT_SIZE]; + padded_input[..input.len()].copy_from_slice(input); + + let (p_bytes, remainder) = padded_input.split_at(ALT_BN128_G1_POINT_SIZE); + let (fr_bytes, _) = remainder.split_at(ALT_BN128_FIELD_SIZE); + + let p = match endianness { + Endianness::BE => PodG1::from_be_bytes(p_bytes)?.into_affine()?, + Endianness::LE => PodG1::from_le_bytes(p_bytes)?.into_affine()?, + }; + + let fr_bytes_array: [u8; ALT_BN128_FIELD_SIZE] = fr_bytes.try_into().ok()?; + let fr_bytes_proper = match endianness { + Endianness::BE => { + swap_endianness::(fr_bytes_array) + } + Endianness::LE => fr_bytes_array, + }; + let fr = BigInteger256::deserialize_uncompressed_unchecked(fr_bytes_proper.as_slice()).ok()?; + + let result_point_affine: G1 = p.mul_bigint(fr).into(); + + let mut result_point_data = [0u8; ALT_BN128_G1_POINT_SIZE]; + result_point_affine + .x + .serialize_with_mode(&mut result_point_data[..ALT_BN128_FIELD_SIZE], Compress::No) + .ok()?; + result_point_affine + .y + .serialize_with_mode(&mut result_point_data[ALT_BN128_FIELD_SIZE..], Compress::No) + .ok()?; + + match endianness { + Endianness::BE => Some(swap_endianness::< + ALT_BN128_FIELD_SIZE, + ALT_BN128_G1_POINT_SIZE, + >(result_point_data)), + Endianness::LE => Some(result_point_data), + } +} + +/// The implementation of the `sol_alt_bn128_group_op` syscall G2 multiplication operation +/// (group operation index 0x06 for BE input/output, 0x86 for LE input/output). +/// +/// **Security Note** +/// +/// Full subgroup (coset) validation is performed on the provided G2 point. +/// +/// **Warning** +/// +/// This is consensus-critical Agave validator code. Modifying this function can +/// result in a network fork. See the [crate-level documentation](crate) for strict +/// guidelines on SIMD approvals and versioning. +pub fn alt_bn128_versioned_g2_multiplication( + _version: VersionedG2Multiplication, + input: &[u8], + endianness: Endianness, +) -> Option<[u8; ALT_BN128_G2_POINT_SIZE]> { + if input.len() != ALT_BN128_G2_MULTIPLICATION_INPUT_SIZE { + return None; + } + + let (p_bytes, fr_bytes) = input.split_at(ALT_BN128_G2_POINT_SIZE); + + let p = match endianness { + Endianness::BE => PodG2::from_be_bytes(p_bytes)?.into_affine()?, + Endianness::LE => PodG2::from_le_bytes(p_bytes)?.into_affine()?, + }; + + let fr_bytes_array: [u8; ALT_BN128_FIELD_SIZE] = fr_bytes.try_into().ok()?; + let fr_bytes_proper = match endianness { + Endianness::BE => { + swap_endianness::(fr_bytes_array) + } + Endianness::LE => fr_bytes_array, + }; + let fr = BigInteger256::deserialize_uncompressed_unchecked(fr_bytes_proper.as_slice()).ok()?; + + let result_point_affine: G2 = p.mul_bigint(fr).into(); + + let mut result_point_data = [0u8; ALT_BN128_G2_POINT_SIZE]; + result_point_affine + .x + .serialize_with_mode(&mut result_point_data[..ALT_BN128_FQ2_SIZE], Compress::No) + .ok()?; + result_point_affine + .y + .serialize_with_mode(&mut result_point_data[ALT_BN128_FQ2_SIZE..], Compress::No) + .ok()?; + + match endianness { + Endianness::BE => { + Some(swap_endianness::(result_point_data)) + } + Endianness::LE => Some(result_point_data), + } +} diff --git a/syscall/bn254-syscall/src/pairing.rs b/syscall/bn254-syscall/src/pairing.rs new file mode 100644 index 0000000..b089966 --- /dev/null +++ b/syscall/bn254-syscall/src/pairing.rs @@ -0,0 +1,86 @@ +use { + crate::{Endianness, PodG1, PodG2, ALT_BN128_G1_POINT_SIZE, ALT_BN128_G2_POINT_SIZE, G1, G2}, + ark_bn254::{self, Config}, + ark_ec::{bn::Bn, pairing::Pairing}, + ark_ff::{BigInteger, BigInteger256, One}, +}; + +/// Pair element size. +pub const ALT_BN128_PAIRING_ELEMENT_SIZE: usize = ALT_BN128_G1_POINT_SIZE + ALT_BN128_G2_POINT_SIZE; // 192 + +/// Output size for pairing operation. +pub const ALT_BN128_PAIRING_OUTPUT_SIZE: usize = 32; + +/// The enum is used to version changes to the `alt_bn128_versioned_pairing` function. +pub enum VersionedPairing { + V0, + /// SIMD-0334 - Fix alt_bn128_pairing Syscall Length Check + V1, +} + +/// The implementation of the `sol_alt_bn128_group_op` syscall pairing operation +/// (group operation index 0x03 for BE input/output, 0x83 for LE input/output). +/// +/// **Security Note** +/// +/// Full subgroup (coset) validation is performed on all provided G2 points. For G1 points, +/// because the cofactor is 1, this validation is equivalent to a standard on-curve check. +/// +/// **Warning** +/// +/// This is consensus-critical Agave validator code. Modifying this function can +/// result in a network fork. See the [crate-level documentation](crate) for strict +/// guidelines on SIMD approvals and versioning. +pub fn alt_bn128_versioned_pairing( + version: VersionedPairing, + input: &[u8], + endianness: Endianness, +) -> Option<[u8; ALT_BN128_PAIRING_OUTPUT_SIZE]> { + // reject deprecated variants + if matches!(version, VersionedPairing::V0) { + return None; + } + + #[allow(clippy::manual_is_multiple_of)] + if input.len() % ALT_BN128_PAIRING_ELEMENT_SIZE != 0 { + return None; + } + + let chunks = input.chunks_exact(ALT_BN128_PAIRING_ELEMENT_SIZE); + let mut vec_pairs: Vec<(G1, G2)> = Vec::with_capacity(chunks.len()); + + for chunk in chunks { + let (p_bytes, q_bytes) = chunk.split_at(ALT_BN128_G1_POINT_SIZE); + + let (g1, g2) = match endianness { + Endianness::BE => ( + PodG1::from_be_bytes(p_bytes)?.into_affine()?, + PodG2::from_be_bytes(q_bytes)?.into_affine()?, + ), + Endianness::LE => ( + PodG1::from_le_bytes(p_bytes)?.into_affine()?, + PodG2::from_le_bytes(q_bytes)?.into_affine()?, + ), + }; + + vec_pairs.push((g1, g2)); + } + + let res = as Pairing>::multi_pairing( + vec_pairs.iter().map(|pair| pair.0), + vec_pairs.iter().map(|pair| pair.1), + ); + + let result = if res.0 == ark_bn254::Fq12::one() { + BigInteger256::from(1u64) + } else { + BigInteger256::from(0u64) + }; + + let output = match endianness { + Endianness::BE => result.to_bytes_be().try_into().ok()?, + Endianness::LE => result.to_bytes_le().try_into().ok()?, + }; + + Some(output) +}