simd: "0388" title: BLS12-381 Elliptic Curve Syscalls authors: Sam Kim (Anza) category: Standard type: Core status: Review created: 2025-10-28
This proposal introduces a new family of syscalls to provide native support for cryptographic operations on the BLS12-381 elliptic curve. These syscalls will expose:
Poseidon hash support is outside the scope of this SIMD.
Solana's current support for pairing-based cryptography is limited to the
alt_bn128 (also known as BN254) curve. Pairing-friendly curves are the
foundation for many efficient and modern zero-knowledge proof systems, such as
those based on Groth16. While the BN254 curve enables basic cryptography and
zero-knowledge applications, it does not provide a 128-bit security level, which
is insufficient for high-security protocols.
The BLS12-381 curve is a modern, widely adopted standard for a pairing-friendly curve that achieves a 128-bit security level. It is adopted in other major ecosystems, such as Ethereum, which already supports it via a precompile.
Adding support for the BLS12-381 curve is important for enabling Alpenglow consensus (SIMD 326) as well. Alpenglow consensus votes themselves do not require a BLS12-381 syscall as they are processed differently than regular transactions.
However, when a validator registers their BLS public key, it must do so using a regular on-chain transaction that will pass through the normal transaction pipeline. This transaction must contain a BLS Proof of Possession (PoP) to validate ownership of the BLS public key. This PoP is a cryptographic proof that is crucial to prevent a "rogue-key-attack", where a malicious validator could try to register a key that is an algebraic combination of their own key and a victim's key. This would allow the attacker to forge aggregate signatures that appear to come from the entire group, even without the victim's participation.
Initially, the vote program will natively execute PoP verification (see SIMD-0387). However, when the vote program eventually transitions to BPF, it will require a way to efficiently verify PoP inside a BPF program. By introducing syscalls for BLS12-381, a standard BPF program can efficiently verify these PoPs.
Proof of Possession (PoP): A cryptographic proof that an entity (e.g., a validator) holds the secret private key corresponding to a public key that they are attempting to register. In the context of BLS, this is typically a signature created with the private key over a message derived from the public key itself.
Rogue-Key Attack: An attack against aggregate signature schemes (like BLS) where a malicious user registers a "rogue" public key that is mathematically constructed from their own key and one or more victim validators' public keys. This allows the attacker to forge aggregate signatures that appear to be validly signed by the entire group (including the victims), even without the victims' participation. Requiring a PoP at registration prevents this attack because the attacker cannot prove possession of the secret key for the rogue public key.
We propose the addition of the following operations on BLS12-381 as syscalls:
These operations are sufficient to enable standard pairing-based cryptography applications, for example, Groth16 proof verifier on BLS12-381 and BLS signature or Proof of Possession (PoP) verifier. Additional operations are needed to support more advanced ZK or cryptography applications, but these are outside the scope of this SIMD.
For the curve definition, the IETF draft should be used as reference. For the encoding, the Zcash specification should be used. We note that the Zcash specification defines a canonical big-endian (BE) encoding. To add the most flexibility, we also define parallel little-endian (LE) variants.
Our LE variants mirror the Zcash standard in structure, with the only change being the byte-ordering of the base field (Fq) elements themselves:
Fq Elements: A 381-bit field element is encoded in 48 bytes.
_BE variants expect this 48-byte array in big-endian._LE variants expect this 48-byte array in little-endian.Fq^2 Element Ordering: An Fq^2 element (c0 + c1 * u) is encoded as 96 bytes.
_BE variants: The Zcash standard's structural ordering is preserved. The
48-byte encoding of the c1 component (imaginary) comes first, followed by
the 48-byte encoding of the c0 component (real)._LE variants: The structural ordering follows the canonical memory layout
of Rust structs and Little-Endian polynomial representation. The 48-byte
encoding of the c0 component (real) comes first, followed by the 48-byte
encoding of the c1 component (imaginary).Compressed Point Control Bits: For compressed representations (used by
sol_curve_decompress), the Zcash standard uses the 3 most-significant-bits
of the entire byte string (e.g., the first bits of the 48-byte G1 array) for
flags (compression, infinity, and sign).
_BE variants: The flags are located in the 3 most significant-bits of the
first byte (offset 0) of the 48-byte array._LE variants: The flags are located in the 3 most-significant-bits of the
last byte (offset 47) of the 48-byte array.We propose using new curve_id constants for BLS12-381. These IDs will be used
across across the new and extended syscalls (sol_curve_group_op,
sol_curve_validate_point, sol_curve_pairing_map, and sol_curve_decompress)
to specify the curve and endianness.
pub const CURVE25519_EDWARDS: u64 = 0;
pub const CURVE25519_RISTRETTO: u64 = 1;
// Reserve indices 2 and 3 in case we want to support affine representations of
// curve25519 points in the future
// pub const CURVE25519_EDWARDS_AFFINE_LE: u64 = 2;
// pub const CURVE25519_EDWARDS_AFFINE_BE: u64 = 2 | 0x80;
// pub const CURVE25519_RISTRETTO_AFFINE_LE: u64 = 3;
// pub const CURVE25519_RISTRETTO_AFFINE_BE: u64 = 3 | 0x80;
// New Curve ID
pub const BLS12_381_LE: u64 = 4;
pub const BLS12_381_BE: u64 = 4 | 0x80;
pub const BLS12_381_G1_LE: u64 = 5;
pub const BLS12_381_G1_BE: u64 = 5 | 0x80;
pub const BLS12_381_G2_LE: u64 = 6;
pub const BLS12_381_G2_BE: u64 = 6 | 0x80;
The BLS12-381_{LE,BE} constants will be used for the pairing operation.
The BLS12_381_G1_{LE,BE} and BLS12_381_G2_{LE,BE} constants will be used for
group operations and decompression.
There is already a dedicated sol_curve_group_op syscall function for general
elliptic curve group operations. This function takes in a curve_id, group_op,
and two scalar/points that are encoded in little-endian. It interprets the
curve points according to the curve id, and applies the group operations
specified by the group_op. Currently, the syscall supports the curve25519
edwards and ristretto representations.
This function can be extended to support the addition, subtraction, and scalar
multiplication in BLS12-381 G1 and G2. The syscall will use the newly defined
BLS12_381_G1_{BE,LE} and BLS12_381_G2_{BE,LE} curve IDs. The syscall will
interpret inputs differently based on the curve_id:
BLS12_381_G1_{BE,LE}, BLS12_381_G2_{BE,LE})
will be interpreted as points in affine representation in either little-endian
or big-endian.CURVE25519_EDWARDS or CURVE25519_RISTRETTO) are
interpreted as points in their respective Edwards or Ristretto representations
in compressed little-endian representations.For clarity, the group_op parameter uses the following existing constants,
which will now also apply to the BLS12-381 curve IDs:
pub const ADD: u64 = 0;
pub const SUB: u64 = 1;
pub const MUL: u64 = 2;
There is an existing definition for a sol_curve_validate_point syscall
function for point validation, which currently supports Curve25519 Edwards and
Ristretto points. We propose extending this syscall to also support validation
for BLS12-381 G1 and G2 points.
define_syscall!(fn sol_curve_validate_point(
curve_id: u64,
point_addr: *const u8,
result: *mut u8
) -> u64)
When called with BLS12_381_G1_{LE,BE} or BLS12_381_G2_{LE,BE} curve IDs,
this function will perform a full validation on the input point, which is
interpreted as an uncompressed affine point (96 bytes for G1, 192 bytes
for G2). The endianness flag (_LE or _BE) dictates how the underlying
field elements are read.
For the operation to succeed (return 0), the input bytes must pass all of the following checks:
p).r.This syscall is specifically for uncompressed affine points. To validate a
compressed point, programs can use the sol_curve_decompress syscall, which
performs identical validation checks as part of its decompression process.
While it is possible to validate an uncompressed affine point (x, y) by using
the sol_curve_decompress function (e.g., by manually setting the control bits
based on y and feeding the x coordinate to the decompressor, then comparing
the result y' with y), this approach is inefficient. It would require the
syscall to perform a costly square root operation to find y'.
The sol_curve_validate_point syscall is more efficient as it can directly
check the curve equation and perform the subgroup check without computing a
square root, making it a cheaper, dedicated operation for on-chain programs that
only need to validate existing affine points.
There is an existing definition for a sol_curve_pairing_map syscall function for
elliptic curve pairing operations.
define_syscall!(fn sol_curve_pairing_map(
curve_id: u64,
point: *const u8,
result: *mut u8
) -> u64);
This function is not actually instantiated at the moment. We propose updating this function's signature to take in an array of points in G1 and an array of points in G2 to support batch pairings.
define_syscall!(fn sol_curve_pairing_map(
curve_id: u64,
num_pairs: u64,
g1_points: *const u8,
g2_points: *const u8,
result: *mut u8
) -> u64);
The num_pairs parameter specifies the number of G1/G2 pairs to be processed.
The g1_points and g2_points parameters are pointers to memory buffers.
The syscall will read num_pairs from each buffer. It must read:
num_pairs * 96 bytes from the g1_points buffer (96 bytes per uncompressed
affine G1 point).num_pairs * 192 bytes from the g2_points buffer (192 bytes per
uncompressed affine G2 point).
If the num_pairs is 0, the syscall should return success with the identity
element of the target group.The runtime must safely handle memory accesses. If reading the required number
of bytes from either buffer results in an out-of-bounds memory access (e.g., the
buffers are smaller than implied by num_pairs), the syscall must return an
error.
The function would interpret g1_points as an array of BLS12-381 points in G1
[P1, ..., Pn] and g2_points as an array of BLS12-381 points in G2
[Q1, ..., Qn]. Both inputs are interpreted as affine representations
encoded in either little-endian or big-endian depending on the curve id. It
should then compute the pairing product e(P1, Q1) * ... * e(Pn, Qn).
The result of the syscall will be the actual target group element from the
product of pairings.
For the decompression operations in G1 and G2, we propose adding a dedicated
syscall for general decompression sol_curve_decompress.
define_syscall!(fn sol_curve_decompress(
curve_id: u64,
point: *const u8,
result: *mut u8
) -> u64);
This function will take in a curve id and a point that is represented in its compressed representation (encoded in little-endian or big-endian). It will decompress the compressed point into an affine representation and write the result.
This function must perform a full point validation as specified in the Zcash specification. For the operation to succeed, the input bytes must pass all of the following checks:
x-coordinate is a valid field element (i.e., less than the
modulus p).r.This proposal extends the existing sol_curve_group_op and
sol_curve_pairing_map syscalls to add support for BLS12-381. The main design
alternative would be to add separate dedicated syscall functions for BLS12-381
as was done for the BN254 curve.
We chose to extend the existing syscalls for few reasons:
It follows the established pattern. The sol_curve_group_op syscall was
designed to be extensible, using a curve_id to handle different curves like
CURVE25519_EDWARDS and CURVE25519_RISTRETTO. Adding a new curve_id for
BLS12-381 is a consistent and logical way to extend it.
It avoids syscall bloat. New curves will keep coming and if we add a new, dedicated syscall for every single one, that would make the runtime hard to maintain and audit. A generic interface is cleaner.
It's a simpler and safer code change. Adding a new curve_id to an existing
match statement is a small, localized, and low-risk change. Plumbing an
entirely new syscall is a much more complex and error-prone task.
This will enable ZK and other more advanced pairing-based cryptography applications to be built on Solana based on a more secure and modern curve.
The necessary security considerations such as point validation, memory safety, etc. are detailed in the Detailed Design section.