Skip to content

Implement BLS12-381 contracttype support #1449

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 16, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions soroban-sdk-macros/src/map_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ use syn::{
Type, TypePath, TypeTuple,
};

// These constants' values must match the definitions of the constants with the
// same names in soroban_sdk::crypto::bls12_381.
pub const FP_SERIALIZED_SIZE: u32 = 48;
pub const FP2_SERIALIZED_SIZE: u32 = FP_SERIALIZED_SIZE * 2;
pub const G1_SERIALIZED_SIZE: u32 = FP_SERIALIZED_SIZE * 2;
pub const G2_SERIALIZED_SIZE: u32 = FP2_SERIALIZED_SIZE * 2;

#[allow(clippy::too_many_lines)]
pub fn map_type(t: &Type, allow_hash: bool) -> Result<ScSpecTypeDef, Error> {
match t {
Expand Down Expand Up @@ -37,6 +44,37 @@ pub fn map_type(t: &Type, allow_hash: bool) -> Result<ScSpecTypeDef, Error> {
"Address" => Ok(ScSpecTypeDef::Address),
"Timepoint" => Ok(ScSpecTypeDef::Timepoint),
"Duration" => Ok(ScSpecTypeDef::Duration),
// The BLS types defined below are represented in the contract's
// interface by their underlying data types, i.e.
// Fp/Fp2/G1Affine/G2Affine => BytesN<N>, Fr => U256. This approach
// simplifies integration with contract development tooling, as it
// avoids introducing new spec types for these BLS constructs.
//
// While this is functionally sound because the BLS types are
// essentially newtypes over their inner representations, it means
// that the specific semantic meaning of `G1Affine`, `G2Affine`, or
// `Fr` is not directly visible in the compiled WASM interface. For
// example, a contract function expecting a `G1Affine` will appear
// in the WASM interface as expecting a `BytesN<96>`.
//
// Future enhancements might allow the macro to automatically deduce
// and utilize the inner types for types defined using the New Type
// Idiom. For more details, see the tracking issue for supporting
// type aliases:
// https://github.com/stellar/rs-soroban-sdk/issues/1063
"Fp" => Ok(ScSpecTypeDef::BytesN(ScSpecTypeBytesN {
n: FP_SERIALIZED_SIZE,
})),
"Fp2" => Ok(ScSpecTypeDef::BytesN(ScSpecTypeBytesN {
n: FP2_SERIALIZED_SIZE,
})),
"G1Affine" => Ok(ScSpecTypeDef::BytesN(ScSpecTypeBytesN {
n: G1_SERIALIZED_SIZE,
})),
"G2Affine" => Ok(ScSpecTypeDef::BytesN(ScSpecTypeBytesN {
n: G2_SERIALIZED_SIZE,
})),
"Fr" => Ok(ScSpecTypeDef::U256),
s => Ok(ScSpecTypeDef::Udt(ScSpecTypeUdt {
name: s.try_into().map_err(|e| {
Error::new(
Expand Down
28 changes: 22 additions & 6 deletions soroban-sdk/src/bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,6 @@ macro_rules! impl_bytesn_repr {
}
}

impl IntoVal<Env, Val> for $elem {
fn into_val(&self, e: &Env) -> Val {
self.0.into_val(e)
}
}

impl TryFromVal<Env, Val> for $elem {
type Error = ConversionError;

Expand All @@ -158,6 +152,28 @@ macro_rules! impl_bytesn_repr {
}
}

impl TryFromVal<Env, $elem> for Val {
type Error = ConversionError;

fn try_from_val(_env: &Env, elt: &$elem) -> Result<Self, Self::Error> {
Ok(elt.to_val())
}
}

#[cfg(not(target_family = "wasm"))]
impl From<&$elem> for ScVal {
fn from(v: &$elem) -> Self {
Self::from(&v.0)
}
}

#[cfg(not(target_family = "wasm"))]
impl From<$elem> for ScVal {
fn from(v: $elem) -> Self {
(&v).into()
}
}

impl IntoVal<Env, BytesN<$size>> for $elem {
fn into_val(&self, _e: &Env) -> BytesN<$size> {
self.0.clone()
Expand Down
60 changes: 50 additions & 10 deletions soroban-sdk/src/crypto/bls12_381.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#[cfg(not(target_family = "wasm"))]
use crate::xdr::ScVal;
use crate::{
env::internal::{self, BytesObject, U256Val, U64Val},
impl_bytesn_repr,
Expand All @@ -12,8 +14,8 @@ use core::{

pub const FP_SERIALIZED_SIZE: usize = 48; // Size in bytes of a serialized Fp element in BLS12-381. The field modulus is 381 bits, requiring 48 bytes (384 bits) with 3 bits reserved for flags.
pub const FP2_SERIALIZED_SIZE: usize = FP_SERIALIZED_SIZE * 2;
pub const G1_SERIALIZED_SIZE: usize = FP_SERIALIZED_SIZE * 2;
pub const G2_SERIALIZED_SIZE: usize = FP2_SERIALIZED_SIZE * 2;
pub const G1_SERIALIZED_SIZE: usize = FP_SERIALIZED_SIZE * 2; // Must match soroban_sdk_macro::map_type::G1_SERIALIZED_SIZE.
pub const G2_SERIALIZED_SIZE: usize = FP2_SERIALIZED_SIZE * 2; // Must match soroban_sdk_macro::map_type::G2_SERIALIZED_SIZE.

/// Bls12_381 provides access to curve and field arithmetics on the BLS12-381
/// curve.
Expand Down Expand Up @@ -203,8 +205,18 @@ impl Fp {
Some(Fp::from_array(self.env(), &bytes))
}

/// Maps to a `G1Affine` point via [simplified SWU
/// mapping](https://www.rfc-editor.org/rfc/rfc9380.html#name-simplified-swu-for-ab-0)
/// Maps this `Fp` element to a `G1Affine` point using the [simplified SWU
/// mapping](https://www.rfc-editor.org/rfc/rfc9380.html#name-simplified-swu-for-ab-0).
///
/// <div class="warning">
/// <h6>Warning</h6>
/// The resulting point is on the curve but may not be in the prime-order subgroup (operations
/// like pairing may fail). To ensure the point is in the prime-order subgroup, cofactor
/// clearing must be performed on the output.
///
/// For applications requiring a point directly in the prime-order subgroup, consider using
/// `hash_to_g1`, which handles subgroup checks and cofactor clearing internally.
/// </div>
pub fn map_to_g1(&self) -> G1Affine {
self.env().crypto().bls12_381().map_fp_to_g1(self)
}
Expand Down Expand Up @@ -330,6 +342,18 @@ impl Fp2 {
Some(Fp2::from_array(self.env(), &inner))
}

/// Maps this `Fp2` element to a `G2Affine` point using the [simplified SWU
/// mapping](https://www.rfc-editor.org/rfc/rfc9380.html#name-simplified-swu-for-ab-0).
///
/// <div class="warning">
/// <h6>Warning</h6>
/// The resulting point is on the curve but may not be in the prime-order subgroup (operations
/// like pairing may fail). To ensure the point is in the prime-order subgroup, cofactor
/// clearing must be performed on the output.
///
/// For applications requiring a point directly in the prime-order subgroup, consider using
/// `hash_to_g2`, which handles subgroup checks and cofactor clearing internally.
/// </div>
pub fn map_to_g2(&self) -> G2Affine {
self.env().crypto().bls12_381().map_fp2_to_g2(self)
}
Expand Down Expand Up @@ -464,12 +488,6 @@ impl From<&Fr> for U256Val {
}
}

impl IntoVal<Env, Val> for Fr {
fn into_val(&self, e: &Env) -> Val {
self.0.into_val(e)
}
}

impl TryFromVal<Env, Val> for Fr {
type Error = ConversionError;

Expand All @@ -479,6 +497,28 @@ impl TryFromVal<Env, Val> for Fr {
}
}

impl TryFromVal<Env, Fr> for Val {
type Error = ConversionError;

fn try_from_val(_env: &Env, fr: &Fr) -> Result<Self, Self::Error> {
Ok(fr.to_val())
}
}

#[cfg(not(target_family = "wasm"))]
impl From<&Fr> for ScVal {
fn from(v: &Fr) -> Self {
Self::from(&v.0)
}
}

#[cfg(not(target_family = "wasm"))]
impl From<Fr> for ScVal {
fn from(v: Fr) -> Self {
(&v).into()
}
}

impl Eq for Fr {}

impl PartialEq for Fr {
Expand Down
72 changes: 69 additions & 3 deletions soroban-sdk/src/tests/crypto_bls12_381.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::{
bytes, bytesn,
use crate::{self as soroban_sdk};
use soroban_sdk::{
bytes, bytesn, contract, contractimpl,
crypto::bls12_381::{Bls12_381, Fp, Fp2, Fr, G1Affine, G2Affine},
vec, Bytes, Env, Vec, U256,
env::EnvTestConfig,
vec, Address, Bytes, BytesN, Env, Vec, U256,
};

#[test]
Expand Down Expand Up @@ -230,3 +232,67 @@ fn test_fr_arithmetic() {
U256::from_u32(&env, 1).into()
);
}

mod blscontract {
use crate as soroban_sdk;
soroban_sdk::contractimport!(file = "../target/wasm32v1-none/release/test_bls.wasm");
}

#[contract]
pub struct Contract;

#[contractimpl(crate_path = "crate")]
impl Contract {
pub fn g1_mul_with(env: Env, contract_id: Address, p: BytesN<96>, s: U256) -> BytesN<96> {
blscontract::Client::new(&env, &contract_id).g1_mul(&p, &s)
}

pub fn verify_with(env: Env, contract_id: Address, proof: blscontract::DummyProof) -> bool {
blscontract::Client::new(&env, &contract_id).dummy_verify(&proof)
}
}

#[test]
fn test_invoke_contract() {
let e = Env::new_with_config(EnvTestConfig {
// Disable test snapshots because the tests in this repo will run across
// multiple hosts, and this test uses a wasm file that won't build consistently
// across different hosts.
capture_snapshot_at_drop: false,
});

let bls_contract_id = e.register(blscontract::WASM, ());

let contract_id = e.register(Contract, ());
let client = ContractClient::new(&e, &contract_id);

// G1 generator and zero scalar
let g1 = G1Affine::from_bytes(bytesn!(&e, 0x17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb08b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1));
let zero = Fr::from_bytes(bytesn!(
&e,
0x0000000000000000000000000000000000000000000000000000000000000000
));
let inf = G1Affine::from_bytes(bytesn!(&e, 0x400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000));
let res = client.g1_mul_with(&bls_contract_id, &g1.as_bytes(), &zero.to_u256());
assert_eq!(&res, inf.as_bytes());

let fp_val = Fp::from_bytes(bytesn!(&e, 0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001));
let fp2_val = Fp2::from_bytes(bytesn!(&e, 0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001));
let g1_point = G1Affine::from_bytes(bytesn!(&e, 0x17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb08b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1));
let g2_point = G2Affine::from_bytes(bytesn!(&e, 0x13e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb80606c4a02ea734cc32acd2b02bc28b99cb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be0ce5d527727d6e118cc9cdc6da2e351aadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801));
let fr_scalar = Fr::from_bytes(bytesn!(
&e,
0x0000000000000000000000000000000000000000000000000000000000000001
));

let proof = blscontract::DummyProof {
fp: fp_val.to_bytes(),
fp2: fp2_val.to_bytes(),
g1: g1_point.to_bytes(),
g2: g2_point.to_bytes(),
fr: fr_scalar.to_u256(),
};

let res = client.verify_with(&bls_contract_id, &proof);
assert!(!res);
}
Loading
Loading