Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add wrapper for BLS from Chia Network #72

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added py_ecc/bls_chia/__init__.py
Empty file.
100 changes: 100 additions & 0 deletions py_ecc/bls_chia/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from typing import (
Sequence,
cast,
)

import blspy as bls_chia

from eth_typing import (
BLSPubkey,
BLSSignature,
Hash32,
)
from eth_utils import (
ValidationError,
)


def _domain_to_bytes(domain: int) -> bytes:
return domain.to_bytes(8, 'big') # bytes8


def _privkey_int_to_bytes(privkey: int) -> bytes:
# FIXME: workaround due to the privkey in Chia-Network BLS starts from 1
return privkey.to_bytes(bls_chia.PrivateKey.PRIVATE_KEY_SIZE, "big")


def _hash_to_be_signed(message_hash: Hash32, domain: int) -> bytes:
domain_in_bytes = _domain_to_bytes(domain)
return message_hash + domain_in_bytes


def sign(message_hash: Hash32,
privkey: int,
domain: int) -> BLSSignature:
privkey_chia = bls_chia.PrivateKey.from_bytes(_privkey_int_to_bytes(privkey))
hash_to_be_signed = _hash_to_be_signed(message_hash, domain)
sig_chia = privkey_chia.sign_insecure(hash_to_be_signed)
sig_chia_bytes = sig_chia.serialize()
return cast(BLSSignature, sig_chia_bytes)


def privtopub(k: int) -> BLSPubkey:
privkey_chia = bls_chia.PrivateKey.from_bytes(_privkey_int_to_bytes(k))
return cast(BLSPubkey, privkey_chia.get_public_key().serialize())


def verify(message_hash: Hash32, pubkey: BLSPubkey, signature: BLSSignature, domain: int) -> bool:
hash_to_be_signed = _hash_to_be_signed(message_hash, domain)
pubkey_chia = bls_chia.PublicKey.from_bytes(pubkey)
signature_chia = bls_chia.Signature.from_bytes(signature)
signature_chia.set_aggregation_info(
bls_chia.AggregationInfo.from_msg(pubkey_chia, hash_to_be_signed)
)
return cast(bool, signature_chia.verify())


def aggregate_signatures(signatures: Sequence[BLSSignature]) -> BLSSignature:
signatures_chia = [
bls_chia.InsecureSignature.from_bytes(signature)
for signature in signatures
]
aggregated_signature = bls_chia.InsecureSignature.aggregate(signatures_chia)
aggregated_signature_bytes = aggregated_signature.serialize()
return cast(BLSSignature, aggregated_signature_bytes)


def aggregate_pubkeys(pubkeys: Sequence[BLSPubkey]) -> BLSPubkey:
pubkeys_chia = list(map(bls_chia.PublicKey.from_bytes, pubkeys))
aggregated_pubkey_chia = bls_chia.PublicKey.aggregate_insecure(pubkeys_chia)
return cast(BLSPubkey, aggregated_pubkey_chia.serialize())


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

message_hashes_with_domain = [
_hash_to_be_signed(message_hash, domain)
for message_hash in message_hashes
]
pubkeys_chia = map(bls_chia.PublicKey.from_bytes, pubkeys)
aggregate_infos = [
bls_chia.AggregationInfo.from_msg(pubkey_chia, message_hash)
for pubkey_chia, message_hash in zip(pubkeys_chia, message_hashes_with_domain)
]
merged_info = bls_chia.AggregationInfo.merge_infos(aggregate_infos)

signature_chia = bls_chia.Signature.from_bytes(signature)
signature_chia.set_aggregation_info(merged_info)
return cast(bool, signature_chia.verify())
81 changes: 81 additions & 0 deletions py_ecc/scripts/benchmark_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import argparse
import importlib
import secrets
import timeit


#
# Parse which module to benchmark
#
parser = argparse.ArgumentParser(
description="Perform benchmark for the bls implementations",
)
parser.add_argument("module", help="which module you would like to test against", type=str)
args = parser.parse_args()
module_path = f"py_ecc.{args.module}.api"
api_module = importlib.import_module(module_path)

#
# Setup
#
num_keys = 20
privkeys = [
# FIXME: should be 8*32, workaround errors with smaller privkeys
secrets.randbits(7 * 32) for _ in range(num_keys)
]
msgs = [
secrets.randbits(8 * 32).to_bytes(32, 'big') for _ in range(num_keys)
]
domain = 5566
pubkeys = [api_module.privtopub(privkey) for privkey in privkeys]
signatures = [
[api_module.sign(msg, privkey, domain) for privkey in privkeys]
for msg in msgs
]
aggregate_multiple_sig = api_module.aggregate_signatures(
[
signature
for msg_signatures in signatures
for signature in msg_signatures
]
)
default_number_bench = 100


def bench(func, number=default_number_bench):
setup = f"from __main__ import api_module, {func.__name__}"
res = timeit.timeit(f"{func.__name__}()", setup=setup, number=number)
print(f"{func.__name__}: {res}")


def privtopub():
api_module.privtopub(privkeys[0])


def verify():
api_module.verify(msgs[0], pubkeys[0], signatures[0][0], domain)


def aggregate_signatures():
api_module.aggregate_signatures(signatures[0])


def aggregate_pubkeys():
api_module.aggregate_pubkeys(pubkeys)


def verify_multiple():
api_module.verify_multiple(
pubkeys=pubkeys,
message_hashes=msgs,
signature=aggregate_multiple_sig,
domain=domain,
)


print(f"Module {module_path}")
bench(privtopub)
bench(verify)
bench(aggregate_signatures)
bench(aggregate_pubkeys)
bench(verify_multiple)
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"eth-typing>=2.1.0,<3.0.0",
"eth-utils>=1.3.0,<2",
"mypy-extensions>=0.4.1",
"blspy>=0.1.8,<1", # for `bls_chia`
],
python_requires='>=3.5, <4',
extras_require=extras_require,
Expand Down
149 changes: 149 additions & 0 deletions tests/test_bls_chia.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import blspy as bls_chia

import pytest

from py_ecc.optimized_bls12_381 import (
G1,
G2,
b,
curve_order,
multiply,
)

from py_ecc.bls_chia.api import (
privtopub,
sign,
verify,
verify_multiple,
aggregate_pubkeys,
aggregate_signatures,
)


def assert_pubkey(obj):
assert isinstance(obj, bytes) and len(obj) == 48


def assert_signature(obj):
assert isinstance(obj, bytes) and len(obj) == 96


def test_sanity():
msg_0 = b"\x32" * 32
domain = 123

# Test: Verify the basic sign/verify process
privkey_0 = 5566
sig_0 = sign(msg_0, privkey_0, domain)
assert_signature(sig_0)
pubkey_0 = privtopub(privkey_0)
assert_pubkey(pubkey_0)
assert verify(msg_0, pubkey_0, sig_0, domain)

privkey_1 = 5567
sig_1 = sign(msg_0, privkey_1, domain)
pubkey_1 = privtopub(privkey_1)
assert verify(msg_0, pubkey_1, sig_1, domain)

# Test: Verify signatures are correctly aggregated
aggregated_signature = aggregate_signatures([sig_0, sig_1])
assert_signature(aggregated_signature)

# Test: Verify pubkeys are correctly aggregated
aggregated_pubkey = aggregate_pubkeys([pubkey_0, pubkey_1])
assert_pubkey(aggregated_pubkey)

# Test: Verify with `aggregated_signature` and `aggregated_pubkey`
assert verify(msg_0, aggregated_pubkey, aggregated_signature, domain)

# Test: `verify_multiple`
msg_1 = b"x22" * 32
privkey_2 = 55688
sig_2 = sign(msg_1, privkey_2, domain)
assert_signature(sig_2)
pubkey_2 = privtopub(privkey_2)
assert_pubkey(pubkey_2)
sig_1_2 = aggregate_signatures([sig_1, sig_2])
assert verify_multiple(
pubkeys=[pubkey_1, pubkey_2],
message_hashes=[msg_0, msg_1],
signature=sig_1_2,
domain=domain,
)


@pytest.mark.parametrize(
'privkey',
[
(1),
(5),
(124),
(735),
(127409812145),
(90768492698215092512159),
(curve_order - 1),
]
)
def test_bls_core(privkey):
domain = 0
p1 = multiply(G1, privkey)
p2 = multiply(G2, privkey)
msg = str(privkey).encode('utf-8')
sig = sign(msg, privkey, domain=domain)
pub = privtopub(privkey)
assert verify(msg, pub, sig, domain=domain)


@pytest.mark.parametrize(
'msg, privkeys',
[
(b'\x12' * 32, [1, 5, 124, 735, 127409812145, 90768492698215092512159, curve_order - 1]),
(b'\x34' * 32, [42, 666, 1274099945, 4389392949595]),
]
)
def test_signature_aggregation(msg, privkeys):
domain = 0
sigs = [sign(msg, k, domain=domain) for k in privkeys]
pubs = [privtopub(k) for k in privkeys]
aggsig = aggregate_signatures(sigs)
aggpub = aggregate_pubkeys(pubs)
assert verify(msg, aggpub, aggsig, domain=domain)


@pytest.mark.parametrize(
'msg_1, msg_2',
[
(b'\x12' * 32, b'\x34' * 32)
]
)
@pytest.mark.parametrize(
'privkeys_1, privkeys_2',
[
(tuple(range(1, 11)), tuple(range(1, 11))),
((1, 2, 3), (4, 5, 6, 7)),
((1, 2, 3), (2, 3, 4, 5)),
]
)
def test_multi_aggregation(msg_1, msg_2, privkeys_1, privkeys_2):
domain = 0

sigs_1 = [sign(msg_1, k, domain=domain) for k in privkeys_1] # signatures to msg_1
pubs_1 = [privtopub(k) for k in privkeys_1]
aggsig_1 = aggregate_signatures(sigs_1)
aggpub_1 = aggregate_pubkeys(pubs_1) # sig_1 to msg_1

sigs_2 = [sign(msg_2, k, domain=domain) for k in privkeys_2] # signatures to msg_2
pubs_2 = [privtopub(k) for k in privkeys_2]
aggsig_2 = aggregate_signatures(sigs_2)
aggpub_2 = aggregate_pubkeys(pubs_2) # sig_2 to msg_2

message_hashes = [msg_1, msg_2]
pubs = [aggpub_1, aggpub_2]
aggsig = aggregate_signatures([aggsig_1, aggsig_2])

assert verify_multiple(
pubkeys=pubs,
message_hashes=message_hashes,
signature=aggsig,
domain=domain,
)