Skip to content

Commit 61b7d6f

Browse files
Implement BLS12-381 contracttype support (#1449)
### What Add support for BLS12-381 curve points (`Fp`, `Fp2`, `G1Affine`, `G2Affine`, `Fr`) for internal data storage (via `contracttype`) and contract invocation arguments (contract spec). ### Why Improves usability of the BLS12-381 features in a Groth16 verifier application ([example contract](stellar/soroban-examples#350)) - `Arbitrary` and several missing conversions are needed to use `G1Affine`, `G2Affine` as contract data. ### Known limitations [TODO or N/A] --------- Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com>
1 parent 13263e8 commit 61b7d6f

File tree

12 files changed

+680
-22
lines changed

12 files changed

+680
-22
lines changed

.github/workflows/rust.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ jobs:
5757
steps:
5858
- uses: actions/checkout@v3
5959
- run: rustup update
60-
- uses: stellar/binaries@v37
60+
- uses: stellar/binaries@v38
6161
with:
6262
name: cargo-semver-checks
63-
version: 0.40.0
63+
version: 0.41.0
6464
- run: cargo semver-checks
6565

6666
build-and-test:

Cargo.lock

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

soroban-sdk-macros/src/map_type.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ use syn::{
88
Type, TypePath, TypeTuple,
99
};
1010

11+
// These constants' values must match the definitions of the constants with the
12+
// same names in soroban_sdk::crypto::bls12_381.
13+
pub const FP_SERIALIZED_SIZE: u32 = 48;
14+
pub const FP2_SERIALIZED_SIZE: u32 = FP_SERIALIZED_SIZE * 2;
15+
pub const G1_SERIALIZED_SIZE: u32 = FP_SERIALIZED_SIZE * 2;
16+
pub const G2_SERIALIZED_SIZE: u32 = FP2_SERIALIZED_SIZE * 2;
17+
1118
#[allow(clippy::too_many_lines)]
1219
pub fn map_type(t: &Type, allow_hash: bool) -> Result<ScSpecTypeDef, Error> {
1320
match t {
@@ -37,6 +44,37 @@ pub fn map_type(t: &Type, allow_hash: bool) -> Result<ScSpecTypeDef, Error> {
3744
"Address" => Ok(ScSpecTypeDef::Address),
3845
"Timepoint" => Ok(ScSpecTypeDef::Timepoint),
3946
"Duration" => Ok(ScSpecTypeDef::Duration),
47+
// The BLS types defined below are represented in the contract's
48+
// interface by their underlying data types, i.e.
49+
// Fp/Fp2/G1Affine/G2Affine => BytesN<N>, Fr => U256. This approach
50+
// simplifies integration with contract development tooling, as it
51+
// avoids introducing new spec types for these BLS constructs.
52+
//
53+
// While this is functionally sound because the BLS types are
54+
// essentially newtypes over their inner representations, it means
55+
// that the specific semantic meaning of `G1Affine`, `G2Affine`, or
56+
// `Fr` is not directly visible in the compiled WASM interface. For
57+
// example, a contract function expecting a `G1Affine` will appear
58+
// in the WASM interface as expecting a `BytesN<96>`.
59+
//
60+
// Future enhancements might allow the macro to automatically deduce
61+
// and utilize the inner types for types defined using the New Type
62+
// Idiom. For more details, see the tracking issue for supporting
63+
// type aliases:
64+
// https://github.com/stellar/rs-soroban-sdk/issues/1063
65+
"Fp" => Ok(ScSpecTypeDef::BytesN(ScSpecTypeBytesN {
66+
n: FP_SERIALIZED_SIZE,
67+
})),
68+
"Fp2" => Ok(ScSpecTypeDef::BytesN(ScSpecTypeBytesN {
69+
n: FP2_SERIALIZED_SIZE,
70+
})),
71+
"G1Affine" => Ok(ScSpecTypeDef::BytesN(ScSpecTypeBytesN {
72+
n: G1_SERIALIZED_SIZE,
73+
})),
74+
"G2Affine" => Ok(ScSpecTypeDef::BytesN(ScSpecTypeBytesN {
75+
n: G2_SERIALIZED_SIZE,
76+
})),
77+
"Fr" => Ok(ScSpecTypeDef::U256),
4078
s => Ok(ScSpecTypeDef::Udt(ScSpecTypeUdt {
4179
name: s.try_into().map_err(|e| {
4280
Error::new(

soroban-sdk/src/bytes.rs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,6 @@ macro_rules! impl_bytesn_repr {
143143
}
144144
}
145145

146-
impl IntoVal<Env, Val> for $elem {
147-
fn into_val(&self, e: &Env) -> Val {
148-
self.0.into_val(e)
149-
}
150-
}
151-
152146
impl TryFromVal<Env, Val> for $elem {
153147
type Error = ConversionError;
154148

@@ -158,6 +152,28 @@ macro_rules! impl_bytesn_repr {
158152
}
159153
}
160154

155+
impl TryFromVal<Env, $elem> for Val {
156+
type Error = ConversionError;
157+
158+
fn try_from_val(_env: &Env, elt: &$elem) -> Result<Self, Self::Error> {
159+
Ok(elt.to_val())
160+
}
161+
}
162+
163+
#[cfg(not(target_family = "wasm"))]
164+
impl From<&$elem> for ScVal {
165+
fn from(v: &$elem) -> Self {
166+
Self::from(&v.0)
167+
}
168+
}
169+
170+
#[cfg(not(target_family = "wasm"))]
171+
impl From<$elem> for ScVal {
172+
fn from(v: $elem) -> Self {
173+
(&v).into()
174+
}
175+
}
176+
161177
impl IntoVal<Env, BytesN<$size>> for $elem {
162178
fn into_val(&self, _e: &Env) -> BytesN<$size> {
163179
self.0.clone()

soroban-sdk/src/crypto/bls12_381.rs

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#[cfg(not(target_family = "wasm"))]
2+
use crate::xdr::ScVal;
13
use crate::{
24
env::internal::{self, BytesObject, U256Val, U64Val},
35
impl_bytesn_repr,
@@ -12,8 +14,8 @@ use core::{
1214

1315
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.
1416
pub const FP2_SERIALIZED_SIZE: usize = FP_SERIALIZED_SIZE * 2;
15-
pub const G1_SERIALIZED_SIZE: usize = FP_SERIALIZED_SIZE * 2;
16-
pub const G2_SERIALIZED_SIZE: usize = FP2_SERIALIZED_SIZE * 2;
17+
pub const G1_SERIALIZED_SIZE: usize = FP_SERIALIZED_SIZE * 2; // Must match soroban_sdk_macro::map_type::G1_SERIALIZED_SIZE.
18+
pub const G2_SERIALIZED_SIZE: usize = FP2_SERIALIZED_SIZE * 2; // Must match soroban_sdk_macro::map_type::G2_SERIALIZED_SIZE.
1719

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

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

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

467-
impl IntoVal<Env, Val> for Fr {
468-
fn into_val(&self, e: &Env) -> Val {
469-
self.0.into_val(e)
470-
}
471-
}
472-
473491
impl TryFromVal<Env, Val> for Fr {
474492
type Error = ConversionError;
475493

@@ -479,6 +497,28 @@ impl TryFromVal<Env, Val> for Fr {
479497
}
480498
}
481499

500+
impl TryFromVal<Env, Fr> for Val {
501+
type Error = ConversionError;
502+
503+
fn try_from_val(_env: &Env, fr: &Fr) -> Result<Self, Self::Error> {
504+
Ok(fr.to_val())
505+
}
506+
}
507+
508+
#[cfg(not(target_family = "wasm"))]
509+
impl From<&Fr> for ScVal {
510+
fn from(v: &Fr) -> Self {
511+
Self::from(&v.0)
512+
}
513+
}
514+
515+
#[cfg(not(target_family = "wasm"))]
516+
impl From<Fr> for ScVal {
517+
fn from(v: Fr) -> Self {
518+
(&v).into()
519+
}
520+
}
521+
482522
impl Eq for Fr {}
483523

484524
impl PartialEq for Fr {

soroban-sdk/src/tests/crypto_bls12_381.rs

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
use crate::{
2-
bytes, bytesn,
1+
use crate::{self as soroban_sdk};
2+
use soroban_sdk::{
3+
bytes, bytesn, contract, contractimpl,
34
crypto::bls12_381::{Bls12_381, Fp, Fp2, Fr, G1Affine, G2Affine},
4-
vec, Bytes, Env, Vec, U256,
5+
env::EnvTestConfig,
6+
vec, Address, Bytes, BytesN, Env, Vec, U256,
57
};
68

79
#[test]
@@ -230,3 +232,67 @@ fn test_fr_arithmetic() {
230232
U256::from_u32(&env, 1).into()
231233
);
232234
}
235+
236+
mod blscontract {
237+
use crate as soroban_sdk;
238+
soroban_sdk::contractimport!(file = "../target/wasm32v1-none/release/test_bls.wasm");
239+
}
240+
241+
#[contract]
242+
pub struct Contract;
243+
244+
#[contractimpl(crate_path = "crate")]
245+
impl Contract {
246+
pub fn g1_mul_with(env: Env, contract_id: Address, p: BytesN<96>, s: U256) -> BytesN<96> {
247+
blscontract::Client::new(&env, &contract_id).g1_mul(&p, &s)
248+
}
249+
250+
pub fn verify_with(env: Env, contract_id: Address, proof: blscontract::DummyProof) -> bool {
251+
blscontract::Client::new(&env, &contract_id).dummy_verify(&proof)
252+
}
253+
}
254+
255+
#[test]
256+
fn test_invoke_contract() {
257+
let e = Env::new_with_config(EnvTestConfig {
258+
// Disable test snapshots because the tests in this repo will run across
259+
// multiple hosts, and this test uses a wasm file that won't build consistently
260+
// across different hosts.
261+
capture_snapshot_at_drop: false,
262+
});
263+
264+
let bls_contract_id = e.register(blscontract::WASM, ());
265+
266+
let contract_id = e.register(Contract, ());
267+
let client = ContractClient::new(&e, &contract_id);
268+
269+
// G1 generator and zero scalar
270+
let g1 = G1Affine::from_bytes(bytesn!(&e, 0x17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb08b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1));
271+
let zero = Fr::from_bytes(bytesn!(
272+
&e,
273+
0x0000000000000000000000000000000000000000000000000000000000000000
274+
));
275+
let inf = G1Affine::from_bytes(bytesn!(&e, 0x400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000));
276+
let res = client.g1_mul_with(&bls_contract_id, &g1.as_bytes(), &zero.to_u256());
277+
assert_eq!(&res, inf.as_bytes());
278+
279+
let fp_val = Fp::from_bytes(bytesn!(&e, 0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001));
280+
let fp2_val = Fp2::from_bytes(bytesn!(&e, 0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001));
281+
let g1_point = G1Affine::from_bytes(bytesn!(&e, 0x17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb08b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1));
282+
let g2_point = G2Affine::from_bytes(bytesn!(&e, 0x13e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb80606c4a02ea734cc32acd2b02bc28b99cb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be0ce5d527727d6e118cc9cdc6da2e351aadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801));
283+
let fr_scalar = Fr::from_bytes(bytesn!(
284+
&e,
285+
0x0000000000000000000000000000000000000000000000000000000000000001
286+
));
287+
288+
let proof = blscontract::DummyProof {
289+
fp: fp_val.to_bytes(),
290+
fp2: fp2_val.to_bytes(),
291+
g1: g1_point.to_bytes(),
292+
g2: g2_point.to_bytes(),
293+
fr: fr_scalar.to_u256(),
294+
};
295+
296+
let res = client.verify_with(&bls_contract_id, &proof);
297+
assert!(!res);
298+
}

0 commit comments

Comments
 (0)