Skip to content

Commit

Permalink
Tower pairings & Timelock encryption (#239)
Browse files Browse the repository at this point in the history
  • Loading branch information
feltroidprime authored Oct 30, 2024
1 parent 017e209 commit c741f55
Show file tree
Hide file tree
Showing 59 changed files with 11,761 additions and 177 deletions.
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

0 comments on commit c741f55

Please sign in to comment.