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

Tower pairings & Timelock encryption #239

Merged
merged 20 commits into from
Oct 30, 2024
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/cairo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- uses: actions/checkout@v3
- uses: software-mansion/setup-scarb@v1
with:
scarb-version: "2.8.2"
scarb-version: "2.8.4"
- run: scarb fmt --check
working-directory: src/
- run: cd src/ && scarb test
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- name: Setup Scarb
uses: software-mansion/setup-scarb@v1
with:
scarb-version: "2.8.2"
scarb-version: "2.8.4"
- name: Install dependencies
run: make setup

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/hydra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
- name: Set up Scarb
uses: software-mansion/setup-scarb@v1
with:
scarb-version: "2.8.2"
scarb-version: "2.8.4"
- name: Run make rewrite and check for unstaged changes
run: |
source venv/bin/activate
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ To get started with Garaga, you'll need to have some tools and dependencies inst

Ensure you have the following installed:
- [Python 3.10](https://www.python.org/downloads/) - /!\ Make sure `python3.10` is a valid command in your terminal. The core language used for development. Make sure you have the correct dependencies installed (in particular, GMP) for the `fastecdsa` python package. See [here](https://pypi.org/project/fastecdsa/#installing) for linux and [here](https://github.com/AntonKueltz/fastecdsa/issues/74) for macos.
- [Scarb 2.8.2](https://docs.swmansion.com/scarb/download.html) - The Cairo package manager. Comes with Cairo inside. Requires [Rust](https://www.rust-lang.org/tools/install).
- [Scarb 2.8.4](https://docs.swmansion.com/scarb/download.html) - The Cairo package manager. Comes with Cairo inside. Requires [Rust](https://www.rust-lang.org/tools/install).

##### Optionally :

Expand Down
2 changes: 1 addition & 1 deletion docs/gitbook/installation/developer-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ icon: wrench
To work with Garaga, you need the following dependencies : 

* Python 3.10. The command `python3.10` should be available and working in your terminal. 
* [Scarb](https://docs.swmansion.com/scarb/download.html) v2.8.2. 
* [Scarb](https://docs.swmansion.com/scarb/download.html) v2.8.4. 
* [Rust](https://www.rust-lang.org/tools/install)

Simply clone the [repository](https://github.com/keep-starknet-strange/garaga) :
Expand Down
24 changes: 23 additions & 1 deletion hydra/garaga/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,17 @@ def __neg__(self) -> "G1Point":
self.iso_point,
)

def to_pyfelt_list(self) -> list[PyFelt]:
field = get_base_field(self.curve_id.value)
return [field(self.x), field(self.y)]

def serialize_to_cairo(self, name: str, raw: bool = False) -> str:
import garaga.modulo_circuit_structs as structs

return structs.G1PointCircuit(name=name, elmts=self.to_pyfelt_list()).serialize(
raw
)


@dataclass(frozen=True)
class G2Point:
Expand All @@ -847,7 +858,7 @@ def __post_init__(self):
if self.is_infinity():
return
if not self.is_on_curve():
raise ValueError("G2 Point is not on the curve")
raise ValueError(f"G2 Point is not on the curve {self.curve_id}")

@staticmethod
def infinity(curve_id: CurveID) -> "G2Point":
Expand Down Expand Up @@ -956,6 +967,17 @@ def msm(points: list["G2Point"], scalars: list[int]) -> "G2Point":
scalar_mul = functools.reduce(lambda acc, p: acc.add(p), muls)
return scalar_mul

def to_pyfelt_list(self) -> list[PyFelt]:
field = get_base_field(self.curve_id.value)
return [field(x) for x in self.x + self.y]

def serialize_to_cairo(self, name: str, raw: bool = False) -> str:
import garaga.modulo_circuit_structs as structs

return structs.G2PointCircuit(name=name, elmts=self.to_pyfelt_list()).serialize(
raw
)


@dataclass(slots=True)
class G1G2Pair:
Expand Down
10 changes: 5 additions & 5 deletions hydra/garaga/drand/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,10 @@ def deserialize_bls_point(s_string: bytes) -> Union[G1Point, G2Point]:
if len(s_string) == 48: # G1 point (compressed)
field = get_base_field(CurveID.BLS12_381)
y2 = field(x**3 + 4)
y = y2.sqrt()
Y_bit = y.value & 1
if S_bit != Y_bit:
y = -y
if S_bit == 1:
y = y2.sqrt(min_root=False)
else:
y = y2.sqrt(min_root=True)
return G1Point(x, y.value, CurveID.BLS12_381)
elif len(s_string) == 96: # G2 point (compressed)
field = get_base_field(CurveID.BLS12_381, Fp2)
Expand Down Expand Up @@ -282,4 +282,4 @@ def print_all_chain_info() -> dict[DrandNetwork, NetworkInfo]:
def generate_precomputed_lines_code(precomputed_lines: StructArray) -> str:
return f"pub const precomputed_lines: [G2Line; {len(precomputed_lines)//4}] = {precomputed_lines.serialize(raw=True, const=True)};"

print(generate_precomputed_lines_code(precomputed_lines))
# print(generate_precomputed_lines_code(precomputed_lines))
226 changes: 226 additions & 0 deletions hydra/garaga/drand/tlock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import hashlib
import secrets
from dataclasses import dataclass

from garaga.definitions import CURVES, CurveID, G1G2Pair, G1Point, G2Point
from garaga.drand.client import DrandNetwork, digest_func
from garaga.hints.tower_backup import E12
from garaga.signature import hash_to_curve


@dataclass
class CipherText:
U: G2Point
V: bytes
W: bytes

def __post_init__(self):
assert len(self.V) == len(self.W) == 16

def serialize_to_cairo(self):
code = f"""CipherText {{
U: {self.U.serialize_to_cairo(name="U", raw=True)},
V: [{', '.join(f'0x{b:02x}' for b in self.V)}],
W: [{', '.join(f'0x{b:02x}' for b in self.W)}],
}}
"""
return code


def encrypt_for_round(
drand_public_key: G2Point, round: int, message: bytes, debug: bool = False
) -> CipherText:
assert len(message) == 16, f"Message should be 16 bytes: {len(message)}"

msg_at_round = digest_func(round)
# print(f"msg_at_round list of ints: {list(msg_at_round)}")
pt_at_round = hash_to_curve(msg_at_round, CurveID.BLS12_381)

gid: E12 = G1G2Pair.pair([G1G2Pair(p=pt_at_round, q=drand_public_key)])

if debug:
# Use a fixed sigma for debugging:
sigma = b"0000000000000000"
else:
sigma: bytes = secrets.token_bytes(16)

assert len(sigma) == 16

hasher = hashlib.sha256()
hasher.update(b"IBE-H3")
hasher.update(sigma)
hasher.update(message)
r = int.from_bytes(expand_message_drand(hasher.digest(), 32), "little")
# print(f"r list of ints: {r}")
r = r % CURVES[CurveID.BLS12_381.value].n

# U = r * G2
U = G2Point.get_nG(CurveID.BLS12_381, r)

# print(f"U: {U}")
# V = sigma XOR H (rGid)
rGid: E12 = gid**r

# print(f"rGid: {rGid.value_coeffs}")

rgid_serialized = rGid.serialize()
# Print bytes as hex strings:
# print(f"rGid hex : {rgid_serialized.hex()}")
# print(f"rGid serialized: {list(rgid_serialized)} len: {len(rgid_serialized)}")
rgid_hash = hashlib.sha256()
rgid_hash.update(b"IBE-H2")
rgid_hash.update(rgid_serialized)
rgid_hash = rgid_hash.digest()
# Take first 16 bytes only :
rgid_hash = rgid_hash[:16]
# print(f"rgid_hash hex: {rgid_hash.hex()}")

V = bytes([a ^ b for a, b in zip(sigma, rgid_hash)])
# print(f"V hex: {V.hex()}")

# 6. Compute W = M XOR H(sigma)
W = bytes([a ^ b for a, b in zip(message, rgid_hash)])
sigma_hash = hashlib.sha256()
sigma_hash.update(b"IBE-H4")
sigma_hash.update(sigma)
sigma_hash = sigma_hash.digest()[:16] # First 16 bytes only
# print(f"sigma_hash hex: {sigma_hash.hex()}")
W = bytes([a ^ b for a, b in zip(message, sigma_hash)])
# print(f"W hex: {W.hex()}")

return CipherText(U, V, W)


def decrypt_at_round(signature_at_round: G1Point, c: CipherText):
# 1. Compute sigma = V XOR H2(e(rP,Sig))

r_gid: E12 = G1G2Pair.pair([G1G2Pair(p=signature_at_round, q=c.U)])
# for i, v in enumerate(r_gid.value_coeffs):
# print(f"rgid_{i}: {io.int_to_u384(v, as_hex=False)}")
r_gid_serialized = r_gid.serialize()
rgid_hash = hashlib.sha256()

pre_image = bytearray(b"IBE-H2")
pre_image.extend(r_gid_serialized)
rgid_hash.update(pre_image)

# for i in range(0, len(pre_image), 4):
# print(f"pre_image[{i}:{i+4}]: {pre_image[i:i+4].hex()}")

rgid_hash = rgid_hash.digest()

rgid_hash = rgid_hash[:16] # First 16 bytes only

sigma = bytes([a ^ b for a, b in zip(c.V, rgid_hash)])

# print(f"sigma hex: {sigma.hex()}")

# 2. Compute Msg = W XOR H4(sigma)
sigma_hash = hashlib.sha256()
sigma_hash.update(b"IBE-H4")
sigma_hash.update(sigma)
sigma_hash = sigma_hash.digest()[:16] # First 16 bytes only

message = bytes([a ^ b for a, b in zip(c.W, sigma_hash)])
# print(f"message utf-8: {message.decode('utf-8')}")

# 3. Check U = G^r

rh = hashlib.sha256()
rh.update(b"IBE-H3")
rh.update(sigma)
rh.update(message)
rh = rh.digest()
rh = expand_message_drand(rh, 32)

r = int.from_bytes(rh, "little")
r = r % CURVES[CurveID.BLS12_381.value].n
U = G2Point.get_nG(CurveID.BLS12_381, r)
assert U == c.U
return message


def expand_message_drand(msg: bytes, buf_size: int) -> bytes:
BITS_TO_MASK_FOR_BLS12381 = 1
order = CURVES[CurveID.BLS12_381.value].n
for i in range(1, 65536): # u16::MAX is 65535
# Hash iteratively: H(i || msg)
h = hashlib.sha256()
pre_image = bytearray(i.to_bytes(2, byteorder="little"))
pre_image.extend(msg)
h.update(pre_image)
hash_result = bytearray(h.digest())
# Mask the first byte
hash_result[0] >>= BITS_TO_MASK_FOR_BLS12381

reversed_hash = hash_result[::-1]

scalar = int.from_bytes(reversed_hash, "little") % order
if scalar != 0:
return reversed_hash[:buf_size]

raise ValueError(
"You are insanely unlucky and should have been hit by a meteor before now"
)


def write_cairo1_test(msg: bytes, round: int, network: DrandNetwork):
chain_infos = print_all_chain_info()
chain = chain_infos[network]
master = chain.public_key
ciphertext: CipherText = encrypt_for_round(master, round, msg, debug=True)

signature_at_round = get_randomness(chain.hash, round).signature_point
_ = decrypt_at_round(signature_at_round, ciphertext)

comment_with_params_used = f"""
// msg: {msg}
// round: {round}
// network: {network}
"""
code = f"""
#[test]
fn test_decrypt_at_round() {{
{comment_with_params_used}
{signature_at_round.serialize_to_cairo(name="signature_at_round")}
let ciph = {ciphertext.serialize_to_cairo()};
let msg_decrypted = decrypt_at_round(signature_at_round, ciph);
assert(msg_decrypted.span() == [{', '.join(f'0x{b:02x}' for b in msg)}].span(), 'wrong msg');
}}
"""
return code


if __name__ == "__main__":
from garaga.drand.client import DrandNetwork, get_randomness, print_all_chain_info

# chain_infos = print_all_chain_info()
network = DrandNetwork.quicknet
# chain = chain_infos[network]

# master = chain.public_key

round = 128

msg = b"hello\x00\x00\x00\x00\x00\x00\x00\x00abc"
# ciph = encrypt_for_round(master, round, msg)

# # print(f"CipherText: {ciph}")

# # print(f"V : {list(ciph.V)}")
# # print(f"W : {list(ciph.W)}")

# chain = chain_infos[network]
# beacon = get_randomness(chain.hash, round)
# signature_at_round = beacon.signature_point

# print(f"signature_at_round: {signature_at_round}")
# msg_decrypted = decrypt_at_round(signature_at_round, ciph)
# assert msg_decrypted == msg

# print(f"msg utf-8: {msg.decode('utf-8')}")
# print(f"msg_decrypted utf-8: {msg_decrypted.decode('utf-8')}")

code = write_cairo1_test(msg, round, network)

print(code)
6 changes: 5 additions & 1 deletion hydra/garaga/extension_field_modulo_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,13 @@ def __init__(
init_hash: int = None,
hash_input: bool = True,
compilation_mode: int = 0,
generic_circuit: bool = False,
) -> None:
super().__init__(
name=name, curve_id=curve_id, compilation_mode=compilation_mode
name=name,
curve_id=curve_id,
compilation_mode=compilation_mode,
generic_circuit=generic_circuit,
)
self.class_name = "ExtensionFieldModuloCircuit"
self.extension_degree = extension_degree
Expand Down
13 changes: 13 additions & 0 deletions hydra/garaga/hints/tower_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,9 @@ def __mul__(self, other):
z0 = c + b
return E12([z0, z1], self.curve_id)

def conjugate(self):
return E12([self.c0, -self.c1], self.curve_id)

def square(self):
c0 = self.c0 - self.c1
c3 = -(self.c1.mul_by_non_residue()) + self.c0
Expand Down Expand Up @@ -472,6 +475,16 @@ def __pow__(self, p: int):

return result

def serialize(self) -> bytes:
# Implement serialization like ark-ff:
serialized = bytearray()
bit_size = CURVES[self.curve_id].p.bit_length()
byte_size = (bit_size + 7) // 8
for c in self.value_coeffs[::-1]:
serialized.extend(c.to_bytes(byte_size, byteorder="big"))

return bytes(serialized)


def get_tower_object(x: list[PyFelt], curve_id: int, extension_degree: int):
if extension_degree == 2:
Expand Down
Loading
Loading