-
Notifications
You must be signed in to change notification settings - Fork 696
Update BLS signature module #1581
Changes from all commits
c17926f
19ebc7b
c0a3226
30be495
118f20f
9df7231
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,16 @@ | ||
| from typing import ( # noqa: F401 | ||
| Dict, | ||
| Iterable, | ||
| Sequence, | ||
| Tuple, | ||
| Union, | ||
| ) | ||
|
|
||
| from eth_utils import ( | ||
| big_endian_to_int, | ||
| ValidationError, | ||
| ) | ||
|
|
||
| from py_ecc.optimized_bn128 import ( # NOQA | ||
| from py_ecc.optimized_bls12_381 import ( # NOQA | ||
| G1, | ||
| G2, | ||
| Z1, | ||
|
|
@@ -20,124 +24,200 @@ | |
| FQP, | ||
| pairing, | ||
| normalize, | ||
| field_modulus, | ||
| field_modulus as q, | ||
| b, | ||
| b2, | ||
| is_on_curve, | ||
| curve_order, | ||
| final_exponentiate | ||
| ) | ||
| from eth.beacon.utils.hash import hash_ | ||
| from eth.utils.bn128 import ( | ||
| FQP_point_to_FQ2_point, | ||
| ) | ||
|
|
||
|
|
||
| CACHE = {} # type: Dict[bytes, Tuple[FQ2, FQ2, FQ2]] | ||
| # 16th root of unity | ||
| HEX_ROOT = FQ2([21573744529824266246521972077326577680729363968861965890554801909984373949499, | ||
| 16854739155576650954933913186877292401521110422362946064090026408937773542853]) | ||
|
|
||
| G2_cofactor = 305502333931268344200999753193121504214466019254188142667664032982267604182971884026507427359259977847832272839041616661285803823378372096355777062779109 # noqa: E501 | ||
| FQ2_order = q ** 2 - 1 | ||
| eighth_roots_of_unity = [ | ||
| FQ2([1, 1]) ** ((FQ2_order * k) // 8) | ||
| for k in range(8) | ||
| ] | ||
|
|
||
| assert HEX_ROOT ** 8 != FQ2([1, 0]) | ||
| assert HEX_ROOT ** 16 == FQ2([1, 0]) | ||
|
|
||
|
|
||
| def compress_G1(pt: Tuple[FQ, FQ, FQ]) -> int: | ||
| x, y = normalize(pt) | ||
| return x.n + 2**255 * (y.n % 2) | ||
| # | ||
| # Helpers | ||
| # | ||
| def FQP_point_to_FQ2_point(pt: Tuple[FQP, FQP, FQP]) -> Tuple[FQ2, FQ2, FQ2]: | ||
| """ | ||
| Transform FQP to FQ2 for type hinting. | ||
| """ | ||
| return ( | ||
| FQ2(pt[0].coeffs), | ||
| FQ2(pt[1].coeffs), | ||
| FQ2(pt[2].coeffs), | ||
| ) | ||
|
|
||
|
|
||
| def decompress_G1(p: int) -> Tuple[FQ, FQ, FQ]: | ||
| if p == 0: | ||
| return (FQ(1), FQ(1), FQ(0)) | ||
| x = p % 2**255 | ||
| y_mod_2 = p // 2**255 | ||
| y = pow((x**3 + b.n) % field_modulus, (field_modulus + 1) // 4, field_modulus) | ||
| assert pow(y, 2, field_modulus) == (x**3 + b.n) % field_modulus | ||
| if y % 2 != y_mod_2: | ||
| y = field_modulus - y | ||
| return (FQ(x), FQ(y), FQ(1)) | ||
| def modular_squareroot(value: int) -> FQP: | ||
| """ | ||
| ``modular_squareroot(x)`` returns the value ``y`` such that ``y**2 % q == x``, | ||
| and None if this is not possible. In cases where there are two solutions, | ||
| the value with higher imaginary component is favored; | ||
| if both solutions have equal imaginary component the value with higher real | ||
| component is favored. | ||
| """ | ||
| candidate_squareroot = value ** ((FQ2_order + 8) // 16) | ||
| check = candidate_squareroot ** 2 / value | ||
| if check in eighth_roots_of_unity[::2]: | ||
| x1 = candidate_squareroot / eighth_roots_of_unity[eighth_roots_of_unity.index(check) // 2] | ||
| x2 = FQ2([-x1.coeffs[0], -x1.coeffs[1]]) # x2 = -x1 | ||
| return x1 if (x1.coeffs[1], x1.coeffs[0]) > (x2.coeffs[1], x2.coeffs[0]) else x2 | ||
| return None | ||
|
|
||
|
|
||
| def sqrt_fq2(x: FQP) -> FQ2: | ||
| y = x ** ((field_modulus ** 2 + 15) // 32) | ||
| while y**2 != x: | ||
| y *= HEX_ROOT | ||
| return FQ2(y.coeffs) | ||
| def hash_to_G2(message: bytes, domain: int) -> Tuple[FQ2, FQ2, FQ2]: | ||
| domain_in_bytes = domain.to_bytes(8, 'big') | ||
|
|
||
| # Initial candidate x coordinate | ||
| x_re = big_endian_to_int(hash_(domain_in_bytes + b'\x01' + message)) | ||
| x_im = big_endian_to_int(hash_(domain_in_bytes + b'\x02' + message)) | ||
| x_coordinate = FQ2([x_re, x_im]) # x_re + x_im * i | ||
|
|
||
| def hash_to_G2(m: bytes) -> Tuple[FQ2, FQ2, FQ2]: | ||
| """ | ||
| WARNING: this function has not been standardized yet. | ||
| """ | ||
| if m in CACHE: | ||
| return CACHE[m] | ||
| k2 = m | ||
| # Test candidate y coordinates until a one is found | ||
| while 1: | ||
| k1 = hash_(k2) | ||
| k2 = hash_(k1) | ||
| x1 = int.from_bytes(k1, 'big') % field_modulus | ||
| x2 = int.from_bytes(k2, 'big') % field_modulus | ||
| x = FQ2([x1, x2]) | ||
| xcb = x**3 + b2 | ||
| if xcb ** ((field_modulus ** 2 - 1) // 2) == FQ2([1, 0]): | ||
| y_coordinate_squared = x_coordinate ** 3 + FQ2([4, 4]) # The curve is y^2 = x^3 + 4(i + 1) | ||
| y_coordinate = modular_squareroot(y_coordinate_squared) | ||
| if y_coordinate is not None: # Check if quadratic residue found | ||
| break | ||
| y = sqrt_fq2(xcb) | ||
| x_coordinate += FQ2([1, 0]) # Add 1 and try again | ||
|
|
||
| o = FQP_point_to_FQ2_point(multiply((x, y, FQ2([1, 0])), 2 * field_modulus - curve_order)) | ||
| CACHE[m] = o | ||
| return o | ||
| return multiply( | ||
| (x_coordinate, y_coordinate, FQ2([1, 0])), | ||
djrtwo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| G2_cofactor | ||
| ) | ||
|
|
||
|
|
||
| # | ||
| # G1 | ||
| # | ||
| def compress_G1(pt: Tuple[FQ, FQ, FQ]) -> int: | ||
| x, y = normalize(pt) | ||
| return x.n + 2**383 * (y.n % 2) | ||
|
|
||
|
|
||
| def decompress_G1(pt: int) -> Tuple[FQ, FQ, FQ]: | ||
| if pt == 0: | ||
| return (FQ(1), FQ(1), FQ(0)) | ||
| x = pt % 2**383 | ||
| y_mod_2 = pt // 2**383 | ||
| y = pow((x**3 + b.n) % q, (q + 1) // 4, q) | ||
|
|
||
| if pow(y, 2, q) != (x**3 + b.n) % q: | ||
| raise ValueError( | ||
| "he given point is not on G1: y**2 = x**3 + b" | ||
| ) | ||
| if y % 2 != y_mod_2: | ||
| y = q - y | ||
| return (FQ(x), FQ(y), FQ(1)) | ||
|
|
||
|
|
||
| # | ||
| # G2 | ||
| # | ||
| def compress_G2(pt: Tuple[FQP, FQP, FQP]) -> Tuple[int, int]: | ||
| assert is_on_curve(pt, b2) | ||
| if not is_on_curve(pt, b2): | ||
| raise ValueError( | ||
| "The given point is not on the twisted curve over FQ**2" | ||
| ) | ||
| x, y = normalize(pt) | ||
| return ( | ||
| int(x.coeffs[0] + 2**255 * (y.coeffs[0] % 2)), | ||
| int(x.coeffs[0] + 2**383 * (y.coeffs[0] % 2)), | ||
| int(x.coeffs[1]) | ||
| ) | ||
|
|
||
|
|
||
| def decompress_G2(p: bytes) -> Tuple[FQP, FQP, FQP]: | ||
| x1 = p[0] % 2**255 | ||
| y1_mod_2 = p[0] // 2**255 | ||
| x1 = p[0] % 2**383 | ||
| y1_mod_2 = p[0] // 2**383 | ||
| x2 = p[1] | ||
| x = FQ2([x1, x2]) | ||
| if x == FQ2([0, 0]): | ||
| return FQ2([1, 0]), FQ2([1, 0]), FQ2([0, 0]) | ||
| y = sqrt_fq2(x**3 + b2) | ||
| y = modular_squareroot(x**3 + b2) | ||
| if y.coeffs[0] % 2 != y1_mod_2: | ||
| y = FQ2((y * -1).coeffs) | ||
| assert is_on_curve((x, y, FQ2([1, 0])), b2) | ||
| if not is_on_curve((x, y, FQ2([1, 0])), b2): | ||
| raise ValueError( | ||
| "The given point is not on the twisted curve over FQ**2" | ||
| ) | ||
| return x, y, FQ2([1, 0]) | ||
|
|
||
|
|
||
| def sign(m: bytes, k: int) -> Tuple[int, int]: | ||
| return compress_G2(multiply(hash_to_G2(m), k)) | ||
| # | ||
| # APIs | ||
| # | ||
| def sign(message: bytes, | ||
| privkey: int, | ||
| domain: int) -> Tuple[int, int]: | ||
| return compress_G2( | ||
| multiply( | ||
| hash_to_G2(message, domain), | ||
| privkey | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def privtopub(k: int) -> int: | ||
| return compress_G1(multiply(G1, k)) | ||
|
|
||
|
|
||
| def verify(m: bytes, pub: int, sig: bytes) -> bool: | ||
| def verify(message: bytes, pubkey: int, signature: bytes, domain: int) -> bool: | ||
| final_exponentiation = final_exponentiate( | ||
| pairing(FQP_point_to_FQ2_point(decompress_G2(sig)), G1, False) * | ||
| pairing(FQP_point_to_FQ2_point(hash_to_G2(m)), neg(decompress_G1(pub)), False) | ||
| pairing(FQP_point_to_FQ2_point(decompress_G2(signature)), G1, False) * | ||
| pairing( | ||
| FQP_point_to_FQ2_point(hash_to_G2(message, domain)), | ||
| neg(decompress_G1(pubkey)), | ||
| False | ||
| ) | ||
| ) | ||
| return final_exponentiation == FQ12.one() | ||
|
|
||
|
|
||
| def aggregate_sigs(sigs: Iterable[bytes]) -> Tuple[int, int]: | ||
| def aggregate_signatures(signatures: Sequence[bytes]) -> Tuple[int, int]: | ||
| o = Z2 | ||
| for s in sigs: | ||
| for s in signatures: | ||
| o = FQP_point_to_FQ2_point(add(o, decompress_G2(s))) | ||
| return compress_G2(o) | ||
|
|
||
|
|
||
| def aggregate_pubs(pubs: Iterable[int]) -> int: | ||
| def aggregate_pubkeys(pubkeys: Sequence[int]) -> int: | ||
| o = Z1 | ||
| for p in pubs: | ||
| for p in pubkeys: | ||
| o = add(o, decompress_G1(p)) | ||
| return compress_G1(o) | ||
|
|
||
|
|
||
| def verify_multiple(pubkeys: Sequence[int], | ||
| messages: Sequence[bytes], | ||
| signature: bytes, | ||
| domain: int) -> bool: | ||
| len_msgs = len(messages) | ||
|
|
||
| if len(pubkeys) != len_msgs: | ||
| raise ValidationError( | ||
| "len(pubkeys) (%s) should be equal to len(messages) (%s)" % ( | ||
| len(pubkeys), len_msgs | ||
| ) | ||
| ) | ||
|
|
||
| o = FQ12([1] + [0] * 11) | ||
| for m_pubs in set(messages): | ||
| # aggregate the pubs | ||
djrtwo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| group_pub = Z1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I was a bit unclear in the last comment. I meant something like the following which utilizes for message in set(messages):
pubkeys_for_message = [
pubkey for i, pubkey in enumerate(pubkeys)
if messages[i] == message
]
group_pub = aggregate_pubkeys(pubkeys_for_message)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I see! I think I've tried to do that before, if we want to utilize
Alternatively, we might refactor def aggregate_pubkeys(pubkeys: Sequence[int]) -> int:
return compress_G1(_aggregate_pubkeys(pubkeys))
def _aggregate_pubkeys(pubkeys: Sequence[int]) -> Tuple[FQ, FQ, FQ]:
o = Z1
for p in pubkeys:
o = add(o, decompress_G1(p))
return oAnd then make What do you think of it?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh crap, you're right. I'm okay either way. It's not a huge gain in code reuse and this code won't change much once in place.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do whichever you want. I'm going to use the updated work in #1631 in the morning so merge whenever you're ready. |
||
| for i in range(len_msgs): | ||
| if messages[i] == m_pubs: | ||
| group_pub = add(group_pub, decompress_G1(pubkeys[i])) | ||
|
|
||
| o *= pairing(hash_to_G2(m_pubs, domain), group_pub, False) | ||
| o *= pairing(decompress_G2(signature), neg(G1), False) | ||
|
|
||
| final_exponentiation = final_exponentiate(o) | ||
| return final_exponentiation == FQ12.one() | ||
Uh oh!
There was an error while loading. Please reload this page.