Skip to content

Commit 3482bc3

Browse files
committed
use hashToCurve and remove unsafe
1 parent a1288ae commit 3482bc3

File tree

3 files changed

+166
-120
lines changed

3 files changed

+166
-120
lines changed

examples/rust-multisig/src/main.rs

Lines changed: 29 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
use alloy::{primitives::U256, providers::ProviderBuilder, sol, sol_types::SolValue};
2-
use blst::{
3-
blst_bendian_from_fp, blst_fp, blst_fp2, blst_fp_from_bendian, blst_keygen, blst_p1,
4-
blst_p1_affine, blst_p1_to_affine, blst_p2, blst_p2_add_or_double, blst_p2_affine,
5-
blst_p2_from_affine, blst_p2_to_affine, blst_scalar, blst_sign_pk_in_g1, blst_sk_to_pk_in_g1,
6-
};
2+
use blst::min_pk::{AggregateSignature, SecretKey, Signature};
73
use rand::RngCore;
84
use BLS::G2Point;
95

@@ -14,112 +10,41 @@ sol! {
1410
"../../out/BLSMultisig.sol/BLSMultisig.json"
1511
}
1612

17-
impl From<BLS::Fp> for blst_fp {
18-
fn from(value: BLS::Fp) -> Self {
19-
let data = value.abi_encode();
13+
impl From<[u8; 96]> for BLS::G1Point {
14+
fn from(value: [u8; 96]) -> Self {
15+
let mut data = [0u8; 128];
16+
data[16..64].copy_from_slice(&value[0..48]);
17+
data[80..128].copy_from_slice(&value[48..96]);
2018

21-
let mut val = blst_fp::default();
22-
unsafe { blst_fp_from_bendian(&mut val, data[16..].as_ptr()) };
23-
24-
val
25-
}
26-
}
27-
28-
impl From<blst_fp> for BLS::Fp {
29-
fn from(value: blst_fp) -> Self {
30-
let mut data = [0u8; 48];
31-
unsafe { blst_bendian_from_fp(data.as_mut_ptr(), &value) };
32-
33-
Self {
34-
a: U256::from_be_slice(&data[..16]),
35-
b: U256::from_be_slice(&data[16..]),
36-
}
37-
}
38-
}
39-
40-
impl From<BLS::Fp2> for blst_fp2 {
41-
fn from(value: BLS::Fp2) -> Self {
42-
Self {
43-
fp: [value.c0.into(), value.c1.into()],
44-
}
45-
}
46-
}
47-
48-
impl From<blst_fp2> for BLS::Fp2 {
49-
fn from(value: blst_fp2) -> Self {
50-
Self {
51-
c0: value.fp[0].into(),
52-
c1: value.fp[1].into(),
53-
}
54-
}
55-
}
56-
57-
impl From<BLS::G2Point> for blst_p2 {
58-
fn from(value: BLS::G2Point) -> Self {
59-
let b_aff = blst_p2_affine {
60-
x: value.x.into(),
61-
y: value.y.into(),
62-
};
63-
64-
let mut b = blst_p2::default();
65-
unsafe { blst_p2_from_affine(&mut b, &b_aff) };
66-
67-
b
68-
}
69-
}
70-
71-
impl From<blst_p2> for BLS::G2Point {
72-
fn from(value: blst_p2) -> Self {
73-
let mut affine = blst_p2_affine::default();
74-
unsafe { blst_p2_to_affine(&mut affine, &value) };
75-
76-
BLS::G2Point {
77-
x: affine.x.into(),
78-
y: affine.y.into(),
79-
}
19+
BLS::G1Point::abi_decode(&data, false).unwrap()
8020
}
8121
}
8222

83-
impl From<blst_p1> for BLS::G1Point {
84-
fn from(value: blst_p1) -> Self {
85-
let mut affine = blst_p1_affine::default();
86-
unsafe { blst_p1_to_affine(&mut affine, &value) };
23+
impl From<[u8; 192]> for BLS::G2Point {
24+
fn from(value: [u8; 192]) -> Self {
25+
let mut data = [0u8; 256];
26+
data[16..64].copy_from_slice(&value[48..96]);
27+
data[80..128].copy_from_slice(&value[0..48]);
28+
data[144..192].copy_from_slice(&value[144..192]);
29+
data[208..256].copy_from_slice(&value[96..144]);
8730

88-
BLS::G1Point {
89-
x: affine.x.into(),
90-
y: affine.y.into(),
91-
}
31+
BLS::G2Point::abi_decode(&data, false).unwrap()
9232
}
9333
}
9434

9535
/// Generates `num` BLS keys and returns them as a tuple of secret keys and public keys, sorted by public key.
96-
fn generate_keys(num: usize) -> (Vec<blst_scalar>, Vec<BLS::G1Point>) {
36+
fn generate_keys(num: usize) -> (Vec<SecretKey>, Vec<BLS::G1Point>) {
9737
let mut rng = rand::thread_rng();
9838
let mut keys = Vec::with_capacity(num);
9939

10040
for _ in 0..num {
10141
let mut ikm = [0u8; 32];
10242
rng.fill_bytes(&mut ikm);
10343

104-
let key_info: &[u8] = &[];
105-
106-
// secret key
107-
let mut sk = blst_scalar::default();
108-
unsafe {
109-
blst_keygen(
110-
&mut sk,
111-
ikm.as_ptr(),
112-
ikm.len(),
113-
key_info.as_ptr(),
114-
key_info.len(),
115-
)
116-
};
117-
118-
// public key
119-
let mut pk = blst_p1::default();
120-
unsafe { blst_sk_to_pk_in_g1(&mut pk, &sk) }
121-
122-
keys.push((sk, BLS::G1Point::from(pk)));
44+
let sk = SecretKey::key_gen(&ikm, &[]).unwrap();
45+
let pk: BLS::G1Point = sk.sk_to_pk().serialize().into();
46+
47+
keys.push((sk, pk));
12348
}
12449

12550
keys.sort_by(|(_, pk1), (_, pk2)| pk1.cmp(pk2));
@@ -128,24 +53,20 @@ fn generate_keys(num: usize) -> (Vec<blst_scalar>, Vec<BLS::G1Point>) {
12853
}
12954

13055
/// Signs a message with the provided keys and returns the aggregated signature.
131-
fn sign_message(keys: &[blst_scalar], message: blst_p2) -> G2Point {
132-
let mut signatures = Vec::new();
56+
fn sign_message(keys: &[SecretKey], msg: &[u8]) -> G2Point {
57+
let mut sigs = Vec::new();
13358

13459
// create individual signatures
13560
for key in keys {
136-
let mut sig = blst_p2::default();
137-
unsafe { blst_sign_pk_in_g1(&mut sig, &message, key) };
138-
139-
signatures.push(sig);
61+
let sig = key.sign(msg, b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_", &[]);
62+
sigs.push(sig);
14063
}
14164

142-
// aggregate signatures by adding them
143-
let mut agg_sig = signatures.swap_remove(0);
144-
for sig in signatures {
145-
unsafe { blst_p2_add_or_double(&mut agg_sig, &agg_sig, &sig) };
146-
}
65+
let agg_sig = Signature::from_aggregate(
66+
&AggregateSignature::aggregate(sigs.iter().collect::<Vec<_>>().as_slice(), false).unwrap(),
67+
);
14768

148-
agg_sig.into()
69+
agg_sig.serialize().into()
14970
}
15071

15172
#[tokio::main]
@@ -160,15 +81,7 @@ pub async fn main() {
16081

16182
let operation = BLSMultisig::Operation::default();
16283

163-
let point: blst_p2 = multisig
164-
.getOperationPoint(operation.clone())
165-
.call()
166-
.await
167-
.unwrap()
168-
._0
169-
.into();
170-
171-
let signature = sign_message(&keys, point);
84+
let signature = sign_message(&keys, &operation.abi_encode());
17285

17386
let receipt = multisig
17487
.verifyAndExecute(BLSMultisig::SignedOperation {

src/BLSMultisig.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ pragma solidity ^0.8.23;
33

44
import {BLS} from "./sign/BLS.sol";
55

6-
/// @notice BLS-powered multisignature wallet, demonstrating the use of
6+
/// @notice BLS-powered multisignature wallet, demonstrating the use of
77
/// aggregated BLS signatures for verification
8-
/// @dev This is for demonstration purposes only, do not use in production. This contract does
8+
/// @dev This is for demonstration purposes only, do not use in production. This contract does
99
/// not include protection from rogue public-key attacks. You
1010
contract BLSMultisig {
11-
/// @notice Public keys of signers. This may contain a pre-aggregated
11+
/// @notice Public keys of signers. This may contain a pre-aggregated
1212
/// public keys for common sets of signers as well.
1313
mapping(bytes32 => bool) public signers;
1414

@@ -52,7 +52,7 @@ contract BLSMultisig {
5252

5353
/// @notice Maps an operation to a point on G2 which needs to be signed.
5454
function getOperationPoint(Operation memory op) public view returns (BLS.G2Point memory) {
55-
return BLS.MapFp2ToG2(BLS.Fp2(BLS.Fp(0, 0), BLS.Fp(0, uint256(keccak256(abi.encode(op))))));
55+
return BLS.hashToCurveG2(abi.encode(op));
5656
}
5757

5858
/// @notice Accepts an operation signed by a subset of the signers and executes it

src/sign/BLS.sol

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,137 @@ library BLS {
150150
require(success, "MAP_FP2_TO_G2 failed");
151151
return abi.decode(output, (G2Point));
152152
}
153+
154+
/// @notice Computes a point in G2 from a message
155+
/// @dev Uses the eip-2537 precompiles
156+
/// @param message Arbitrarylength byte string to be hashed
157+
/// @return A point in G2
158+
function hashToCurveG2(bytes memory message) internal view returns (G2Point memory) {
159+
// 1. u = hash_to_field(msg, 2)
160+
Fp2[2] memory u = hashToFieldFp2(message, bytes("BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"));
161+
// 2. Q0 = map_to_curve(u[0])
162+
G2Point memory q0 = MapFp2ToG2(u[0]);
163+
// 3. Q1 = map_to_curve(u[1])
164+
G2Point memory q1 = MapFp2ToG2(u[1]);
165+
// 4. R = Q0 + Q1
166+
return G2Add(q0, q1);
167+
}
168+
169+
/// @notice Computes a field point from a message
170+
/// @dev Follows https://datatracker.ietf.org/doc/html/rfc9380#section-5.2
171+
/// @param message Arbitrarylength byte string to be hashed
172+
/// @param dst The domain separation tag
173+
/// @return Two field points
174+
function hashToFieldFp2(bytes memory message, bytes memory dst) private view returns (Fp2[2] memory) {
175+
// 1. len_in_bytes = count * m * L
176+
// so always 2 * 2 * 64 = 256
177+
uint16 lenInBytes = 256;
178+
// 2. uniform_bytes = expand_message(msg, DST, len_in_bytes)
179+
bytes32[] memory pseudoRandomBytes = expandMsgXmd(message, dst, lenInBytes);
180+
Fp2[2] memory u;
181+
// No loop here saves 800 gas hardcoding offset an additional 300
182+
// 3. for i in (0, ..., count - 1):
183+
// 4. for j in (0, ..., m - 1):
184+
// 5. elm_offset = L * (j + i * m)
185+
// 6. tv = substr(uniform_bytes, elm_offset, HTF_L)
186+
// uint8 HTF_L = 64;
187+
// bytes memory tv = new bytes(64);
188+
// 7. e_j = OS2IP(tv) mod p
189+
// 8. u_i = (e_0, ..., e_(m - 1))
190+
// tv = bytes.concat(pseudo_random_bytes[0], pseudo_random_bytes[1]);
191+
u[0].c0 = _modfield(pseudoRandomBytes[0], pseudoRandomBytes[1]);
192+
u[0].c1 = _modfield(pseudoRandomBytes[2], pseudoRandomBytes[3]);
193+
u[1].c0 = _modfield(pseudoRandomBytes[4], pseudoRandomBytes[5]);
194+
u[1].c1 = _modfield(pseudoRandomBytes[6], pseudoRandomBytes[7]);
195+
// 9. return (u_0, ..., u_(count - 1))
196+
return u;
197+
}
198+
199+
/// @notice Computes a field point from a message
200+
/// @dev Follows https://datatracker.ietf.org/doc/html/rfc9380#section-5.3
201+
/// @dev bytes32[] because len_in_bytes is always a multiple of 32 in our case even 128
202+
/// @param message Arbitrarylength byte string to be hashed
203+
/// @param dst The domain separation tag of at most 255 bytes
204+
/// @param lenInBytes The length of the requested output in bytes
205+
/// @return A field point
206+
function expandMsgXmd(bytes memory message, bytes memory dst, uint16 lenInBytes)
207+
private
208+
pure
209+
returns (bytes32[] memory)
210+
{
211+
// 1. ell = ceil(len_in_bytes / b_in_bytes)
212+
// b_in_bytes seems to be 32 for sha256
213+
// ceil the division
214+
uint256 ell = (lenInBytes - 1) / 32 + 1;
215+
216+
// 2. ABORT if ell > 255 or len_in_bytes > 65535 or len(DST) > 255
217+
require(ell <= 255, "len_in_bytes too large for sha256");
218+
// Not really needed because of parameter type
219+
// require(lenInBytes <= 65535, "len_in_bytes too large");
220+
// no length normalizing via hashing
221+
require(dst.length <= 255, "dst too long");
222+
223+
bytes memory dstPrime = bytes.concat(dst, bytes1(uint8(dst.length)));
224+
225+
// 4. Z_pad = I2OSP(0, s_in_bytes)
226+
// this should be sha256 blocksize so 64 bytes
227+
bytes memory zPad = new bytes(64);
228+
229+
// 5. l_i_b_str = I2OSP(len_in_bytes, 2)
230+
// length in byte string?
231+
bytes2 libStr = bytes2(lenInBytes);
232+
233+
// 6. msg_prime = Z_pad || msg || l_i_b_str || I2OSP(0, 1) || DST_prime
234+
bytes memory msgPrime = bytes.concat(zPad, message, libStr, hex"00", dstPrime);
235+
236+
// 7. b_0 = H(msg_prime)
237+
bytes32 b_0 = sha256(msgPrime);
238+
239+
bytes32[] memory b = new bytes32[](ell);
240+
241+
// 8. b_1 = H(b_0 || I2OSP(1, 1) || DST_prime)
242+
b[0] = sha256(bytes.concat(b_0, hex"01", dstPrime));
243+
244+
// 9. for i in (2, ..., ell):
245+
for (uint8 i = 2; i <= ell; i++) {
246+
// 10. b_i = H(strxor(b_0, b_(i - 1)) || I2OSP(i, 1) || DST_prime)
247+
bytes memory tmp = abi.encodePacked(b_0 ^ b[i - 2], i, dstPrime);
248+
b[i - 1] = sha256(tmp);
249+
}
250+
// 11. uniform_bytes = b_1 || ... || b_ell
251+
// 12. return substr(uniform_bytes, 0, len_in_bytes)
252+
// Here we don't need the uniform_bytes because b is already properly formed
253+
return b;
254+
}
255+
256+
// passing two bytes32 instead of bytes memory saves approx 700 gas per call
257+
// Computes the mod against the bls12-381 field modulus
258+
function _modfield(bytes32 _b1, bytes32 _b2) private view returns (Fp memory r) {
259+
(bool success, bytes memory output) = address(0x5).staticcall(
260+
abi.encode(
261+
// arg[0] = base.length
262+
0x40,
263+
// arg[1] = exp.length
264+
0x20,
265+
// arg[2] = mod.length
266+
0x40,
267+
// arg[3] = base.bits @ + 0x60
268+
// places the first 32 bytes of _b1 and the last 32 bytes of _b2
269+
_b1,
270+
_b2,
271+
// arg[4] = exp
272+
// exponent always 1
273+
1,
274+
// arg[5] = mod
275+
// this field_modulus as hex 4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787
276+
// we add the 0 prefix so that the result will be exactly 64 bytes
277+
// saves 300 gas per call instead of sending it along every time
278+
// places the first 32 bytes and the last 32 bytes of the field modulus
279+
0x000000000000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd7, // arg[5] = mod
280+
0x64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab //
281+
)
282+
);
283+
require(success, "MODEXP failed");
284+
return abi.decode(output, (Fp));
285+
}
153286
}

0 commit comments

Comments
 (0)