Skip to content

Commit

Permalink
Add BLS APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
ChihChengLiang committed Feb 27, 2019
1 parent 0b32be4 commit 5d8d404
Show file tree
Hide file tree
Showing 8 changed files with 479 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ logs
# Mongo Explorer plugin:
.idea/mongoSettings.xml

# Mypy cache
.mypy_cache

# VIM temp files
*.swp

Expand Down
Empty file added py_ecc/bls_api/__init__.py
Empty file.
114 changes: 114 additions & 0 deletions py_ecc/bls_api/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from typing import (
Sequence,
)
from eth_utils import (
ValidationError,
)
from py_ecc.optimized_bls12_381 import (
FQ12,
G1,
Z1,
Z2,
add,
final_exponentiate,
multiply,
neg,
pairing,
)
from .typing import (
BLSPubkey,
BLSSignature,
Hash32,
)
from .utils import (
FQP_point_to_FQ2_point,
G1_to_pubkey,
G2_to_signature,
compress_G1,
compress_G2,
decompress_G1,
decompress_G2,
hash_to_G2,
pubkey_to_G1,
signature_to_G2,
)


def sign(message_hash: Hash32,
privkey: int,
domain: int) -> BLSSignature:
return G2_to_signature(
compress_G2(
multiply(
hash_to_G2(message_hash, domain),
privkey
)
))


def privtopub(k: int) -> BLSPubkey:
return G1_to_pubkey(compress_G1(multiply(G1, k)))


def verify(message_hash: Hash32, pubkey: BLSPubkey, signature: BLSSignature, domain: int) -> bool:
try:
final_exponentiation = final_exponentiate(
pairing(
FQP_point_to_FQ2_point(decompress_G2(signature_to_G2(signature))),
G1,
final_exponentiate=False,
) *
pairing(
FQP_point_to_FQ2_point(hash_to_G2(message_hash, domain)),
neg(decompress_G1(pubkey_to_G1(pubkey))),
final_exponentiate=False,
)
)
return final_exponentiation == FQ12.one()
except (ValidationError, ValueError, AssertionError):
return False


def aggregate_signatures(signatures: Sequence[BLSSignature]) -> BLSSignature:
o = Z2
for s in signatures:
o = FQP_point_to_FQ2_point(add(o, decompress_G2(signature_to_G2(s))))
return G2_to_signature(compress_G2(o))


def aggregate_pubkeys(pubkeys: Sequence[BLSPubkey]) -> BLSPubkey:
o = Z1
for p in pubkeys:
o = add(o, decompress_G1(pubkey_to_G1(p)))
return G1_to_pubkey(compress_G1(o))


def verify_multiple(pubkeys: Sequence[BLSPubkey],
message_hashes: Sequence[Hash32],
signature: BLSSignature,
domain: int) -> bool:
len_msgs = len(message_hashes)

if len(pubkeys) != len_msgs:
raise ValidationError(
"len(pubkeys) (%s) should be equal to len(message_hashes) (%s)" % (
len(pubkeys), len_msgs
)
)

try:
o = FQ12([1] + [0] * 11)
for m_pubs in set(message_hashes):
# aggregate the pubs
group_pub = Z1
for i in range(len_msgs):
if message_hashes[i] == m_pubs:
group_pub = add(group_pub, decompress_G1(pubkey_to_G1(pubkeys[i])))

o *= pairing(hash_to_G2(m_pubs, domain), group_pub, final_exponentiate=False)
o *= pairing(decompress_G2(signature_to_G2(signature)), neg(G1), final_exponentiate=False)

final_exponentiation = final_exponentiate(o)
return final_exponentiation == FQ12.one()
except (ValidationError, ValueError, AssertionError):
return False
12 changes: 12 additions & 0 deletions py_ecc/bls_api/hash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from .typing import Hash32
from eth_hash.auto import keccak
from typing import Union


def hash_eth2(data: Union[bytes, bytearray]) -> Hash32:
"""
Return Keccak-256 hashed result.
Note: it's a placeholder and we aim to migrate to a S[T/N]ARK-friendly hash function in
a future Ethereum 2.0 deployment phase.
"""
return Hash32(keccak(data))
10 changes: 10 additions & 0 deletions py_ecc/bls_api/typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This module will not be included in the PR.
# These types should be replaced with those in eth-typing

from typing import (
NewType,
)

Hash32 = NewType("Hash32", bytes)
BLSPubkey = NewType('BLSPubkey', bytes) # bytes48
BLSSignature = NewType('BLSSignature', bytes) # bytes96
174 changes: 174 additions & 0 deletions py_ecc/bls_api/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
from typing import ( # noqa: F401
Dict,
Sequence,
Tuple,
Union,
)

from eth_utils import (
big_endian_to_int,
)
from py_ecc.optimized_bls12_381 import (
FQ,
FQ2,
FQP,
b,
b2,
field_modulus as q,
is_on_curve,
multiply,
normalize,
)

from .hash import (
hash_eth2,
)
from .typing import (
BLSPubkey,
BLSSignature,
Hash32,
)

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)
]


#
# 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 modular_squareroot(value: FQ2) -> 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 _get_x_coordinate(message_hash: Hash32, domain: int) -> FQ2:
domain_in_bytes = domain.to_bytes(8, 'big')

# Initial candidate x coordinate
x_re = big_endian_to_int(hash_eth2(message_hash + domain_in_bytes + b'\x01'))
x_im = big_endian_to_int(hash_eth2(message_hash + domain_in_bytes + b'\x02'))
x_coordinate = FQ2([x_re, x_im]) # x_re + x_im * i

return x_coordinate


def hash_to_G2(message_hash: Hash32, domain: int) -> Tuple[FQ2, FQ2, FQ2]:
x_coordinate = _get_x_coordinate(message_hash, domain)

# Test candidate y coordinates until a one is found
while 1:
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
x_coordinate += FQ2([1, 0]) # Add 1 and try again

return multiply(
(x_coordinate, y_coordinate, FQ2([1, 0])),
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))


def G1_to_pubkey(pt: int) -> BLSPubkey:
return BLSPubkey(pt.to_bytes(48, "big"))


def pubkey_to_G1(pubkey: BLSPubkey) -> int:
return big_endian_to_int(pubkey)

#
# G2
#


def compress_G2(pt: Tuple[FQP, FQP, FQP]) -> Tuple[int, int]:
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**383 * (y.coeffs[0] % 2)),
int(x.coeffs[1])
)


def decompress_G2(p: Tuple[int, int]) -> Tuple[FQP, FQP, FQP]:
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 = modular_squareroot(x**3 + b2)
if y is None:
raise ValueError("Failed to find a modular squareroot")
if y.coeffs[0] % 2 != y1_mod_2:
y = FQ2((y * -1).coeffs)
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 G2_to_signature(pt: Tuple[int, int]) -> BLSSignature:
return BLSSignature(
pt[0].to_bytes(48, "big") +
pt[1].to_bytes(48, "big")
)


def signature_to_G2(signature: BLSSignature) -> Tuple[int, int]:
return (big_endian_to_int(signature[:48]), big_endian_to_int(signature[48:]))
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
packages=find_packages(exclude=('tests', 'docs')),
package_data={'py_ecc': ['py.typed']},
install_requires=[
"eth-hash>=0.1.4,<1",
"eth-utils>=1.3.0,<2",
"eth-typing>=2.0.0,<3.0.0",
],
python_requires='>=3.5, <4',
extras_require=extras_require,
Expand Down
Loading

0 comments on commit 5d8d404

Please sign in to comment.