Skip to content

Commit f4e9932

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

File tree

3 files changed

+168
-120
lines changed

3 files changed

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

0 commit comments

Comments
 (0)