From ffba48381c72602627920194ee9d418afed92f5a Mon Sep 17 00:00:00 2001 From: Prasad Kumkar Date: Fri, 19 Jun 2026 17:45:12 +0530 Subject: [PATCH 1/4] fix: refactor codefor clarity; rm redundancy --- README.md | 29 ++-- dot_ring/__init__.py | 15 +- dot_ring/curve/curve.py | 40 ++++- dot_ring/keygen.py | 24 --- dot_ring/ring_proof/columns/columns.py | 63 +++----- .../ring_proof/constraints/constraints.py | 49 +++--- dot_ring/ring_proof/params.py | 133 ++++------------ dot_ring/ring_proof/ring_curve.py | 48 ++++++ dot_ring/ring_proof/transcript/phases.py | 15 +- dot_ring/ring_proof/verify.py | 40 +++-- dot_ring/vrf/ietf/thin.py | 4 +- dot_ring/vrf/ietf/tiny.py | 24 +-- dot_ring/vrf/io.py | 2 +- dot_ring/vrf/pedersen/__init__.py | 4 +- dot_ring/vrf/pedersen/batch_verifier.py | 22 +-- dot_ring/vrf/pedersen/vrf.py | 24 +-- dot_ring/vrf/ring/__init__.py | 4 +- dot_ring/vrf/ring/batch_verifier.py | 112 +++++-------- dot_ring/vrf/ring/context.py | 4 +- dot_ring/vrf/ring/members.py | 59 +++---- dot_ring/vrf/ring/proof_builder.py | 44 +++--- dot_ring/vrf/ring/proof_payload.py | 91 ++++++----- dot_ring/vrf/ring/root.py | 55 +++++-- dot_ring/vrf/ring/vrf.py | 62 +++----- dot_ring/vrf/transcript.py | 2 +- dot_ring/vrf/vrf.py | 34 +--- scripts/generate_test_vectors.py | 10 +- tests/benchmark/bench_ietf.py | 5 +- tests/benchmark/bench_pedersen.py | 5 +- tests/benchmark/bench_ring_proof.py | 27 ++-- tests/benchmark/memory_regression.py | 5 +- tests/test_ark_vrf.py | 61 ++++--- tests/test_bandersnatch_ark.py | 28 ++-- tests/test_coverage/test_columns.py | 4 +- tests/test_coverage/test_params.py | 2 +- tests/test_curve_ops/test_gaps.py | 50 +++--- tests/test_dot_ring_vectors.py | 12 +- tests/test_ietf/test_ietf_ark.py | 8 +- tests/test_ietf/test_ietf_base.py | 8 +- tests/test_keygen.py | 13 +- tests/test_pedersen/test_pedersen_ark.py | 8 +- tests/test_pedersen/test_pedersen_base.py | 6 +- tests/test_ring_vrf/test_audit_regressions.py | 36 ++--- tests/test_ring_vrf/test_ring_vrf.py | 12 +- tests/test_vectors.py | 20 +-- tests/test_verify_ring_sig.py | 21 +-- .../bandersnatch_sw_sha-512_tai_ring.json | 135 ---------------- .../bandersnatch_sw_sha-512_tai_ring.json | 149 ------------------ 48 files changed, 600 insertions(+), 1028 deletions(-) delete mode 100644 dot_ring/keygen.py create mode 100644 dot_ring/ring_proof/ring_curve.py delete mode 100644 tests/vectors/ark-vrf/bandersnatch_sw_sha-512_tai_ring.json delete mode 100644 tests/vectors/dot-ring/bandersnatch_sw_sha-512_tai_ring.json diff --git a/README.md b/README.md index 48a936d..bf61481 100644 --- a/README.md +++ b/README.md @@ -57,10 +57,10 @@ ad = b"additional data" Deterministic key generation from a seed: ```python -from dot_ring import Bandersnatch, secret_from_seed +from dot_ring import Bandersnatch seed = (0).to_bytes(32, "little") -public_key, secret_scalar = secret_from_seed(seed, Bandersnatch) +public_key, secret_key = Bandersnatch.secret_from_seed(seed) ``` ### Tiny VRF / IETF @@ -72,12 +72,12 @@ from dot_ring import Bandersnatch, TinyVRF proof = TinyVRF[Bandersnatch].prove(alpha, secret_key, ad) # Verify -public_key = TinyVRF[Bandersnatch].get_public_key(secret_key) +public_key = Bandersnatch.public_key_from_secret(secret_key) is_valid = proof.verify(public_key, alpha, ad) # Serialize -proof_bytes = proof.to_bytes() -proof = TinyVRF[Bandersnatch].from_bytes(proof_bytes) +proof_bytes = proof.encode() +proof = TinyVRF[Bandersnatch].decode(proof_bytes) ``` ### Pedersen VRF @@ -95,18 +95,25 @@ is_valid = proof.verify(alpha, ad) ### Ring VRF ```python -from dot_ring import Bandersnatch, RingVRF +from dot_ring import Bandersnatch, RingContext, RingVRF # Setup ring -ring_pks = [pk1, pk2, pk3, ...] # list of public keys -ring_root = RingVRF[Bandersnatch].construct_ring_root(ring_pks) +my_pk = Bandersnatch.public_key_from_secret(secret_key) +other_pks = [ + Bandersnatch.public_key_from_secret(i.to_bytes(32, "little")) + for i in range(1, 4) +] +ring_pks = [my_pk, *other_pks] + +context = RingContext.from_ring_size(len(ring_pks), cv=Bandersnatch) +ring = context.ring(ring_pks) +ring_root = context.ring_root(ring) # Generate proof -my_pk = RingVRF[Bandersnatch].get_public_key(secret_key) -proof = RingVRF[Bandersnatch].prove(alpha, ad, secret_key, my_pk, ring_pks) +proof = RingVRF[Bandersnatch].prove(alpha, ad, secret_key, my_pk, ring, ring_root) # Verify (proves membership without revealing which key) -is_valid = proof.verify(alpha, ad, ring_root) +is_valid = proof.verify(alpha, ad, ring, ring_root) ``` --- diff --git a/dot_ring/__init__.py b/dot_ring/__init__.py index 3696c05..f1c3e1d 100644 --- a/dot_ring/__init__.py +++ b/dot_ring/__init__.py @@ -4,7 +4,7 @@ Supports 10+ elliptic curves including Tiny, Thin, Pedersen, and Ring VRF schemes. Example usage: - >>> from dot_ring import Bandersnatch, TinyVRF, PedersenVRF, RingVRF + >>> from dot_ring import Bandersnatch, RingContext, TinyVRF, PedersenVRF, RingVRF >>> >>> # Tiny VRF >>> proof = TinyVRF[Bandersnatch].prove(alpha, secret_key, additional_data) @@ -15,9 +15,11 @@ >>> is_valid = proof.verify(alpha, additional_data) >>> >>> # Ring VRF - >>> ring_root = RingVRF[Bandersnatch].construct_ring_root(keys_list) - >>> proof = RingVRF[Bandersnatch].prove(alpha, ad, secret_key, producer_key, keys) - >>> is_valid = proof.verify(alpha, ad, ring_root) + >>> context = RingContext.from_ring_size(len(keys), cv=Bandersnatch) + >>> ring = context.ring(keys) + >>> ring_root = context.ring_root(ring) + >>> proof = RingVRF[Bandersnatch].prove(alpha, ad, secret_key, producer_key, ring, ring_root) + >>> is_valid = proof.verify(alpha, ad, ring, ring_root) """ __version__ = "0.1.0" @@ -48,10 +50,9 @@ from dot_ring.curve.specs.p384 import P384_NU, P384_RO from dot_ring.curve.specs.p521 import P521_NU, P521_RO from dot_ring.curve.specs.secp256k1 import Secp256k1_NU, Secp256k1_RO -from dot_ring.keygen import secret_from_seed from dot_ring.vrf.ietf import ThinBatchVerifier, ThinVRF, TinyVRF from dot_ring.vrf.pedersen import PedersenBatchVerifier, PedersenVRF -from dot_ring.vrf.ring import Ring, RingBatchContext, RingBatchVerifier, RingContext, RingRoot, RingRootBuilder, RingVRF +from dot_ring.vrf.ring import Ring, RingBatchVerifier, RingContext, RingRoot, RingRootBuilder, RingVRF # ============================================================================= # Convenience aliases @@ -82,7 +83,6 @@ "RingVRF", "RingContext", "RingRootBuilder", - "RingBatchContext", "RingBatchVerifier", # Primary curves "Bandersnatch", @@ -125,7 +125,6 @@ "BLS12_381_G2", "BLS12_381_G2_RO", "BLS12_381_G2_NU", - "secret_from_seed", "Ring", "RingRoot", ] diff --git a/dot_ring/curve/curve.py b/dot_ring/curve/curve.py index e8114b5..d589673 100644 --- a/dot_ring/curve/curve.py +++ b/dot_ring/curve/curve.py @@ -3,7 +3,7 @@ import hashlib import math from dataclasses import dataclass -from typing import TYPE_CHECKING, Generic, Literal, TypeVar, cast +from typing import Generic, Literal, TypeVar, cast, overload from gmpy2 import invert as _invert from gmpy2 import mpz as _mpz @@ -11,11 +11,9 @@ from dot_ring.curve.e2c import E2C_Variant from dot_ring.curve.fp2 import Fp2 +from dot_ring.curve.point import CurvePoint from dot_ring.curve.specs.parameters import CurveParams, HashConstructor -if TYPE_CHECKING: - from dot_ring.curve.point import CurvePoint - CoordT = TypeVar("CoordT", int, Fp2) @@ -350,7 +348,24 @@ class CurveVariant(Generic[CoordT]): curve: Curve[CoordT] point_type: type[CurvePoint[Curve[CoordT], CoordT]] - def point(self, x: CoordT, y: CoordT) -> CurvePoint[Curve[CoordT], CoordT]: + @overload + def point(self, x: CoordT, y: CoordT) -> CurvePoint[Curve[CoordT], CoordT]: ... + + @overload + def point(self, x: tuple[CoordT, CoordT]) -> CurvePoint[Curve[CoordT], CoordT]: ... + + @overload + def point(self, x: CurvePoint[Curve[CoordT], CoordT]) -> CurvePoint[Curve[CoordT], CoordT]: ... + + def point( + self, + x: CoordT | tuple[CoordT, CoordT] | CurvePoint[Curve[CoordT], CoordT], + y: CoordT | None = None, + ) -> CurvePoint[Curve[CoordT], CoordT]: + if isinstance(x, CurvePoint): + return x + if y is None: + x, y = x return self.point_type(x, y, self.curve) def identity(self) -> CurvePoint[Curve[CoordT], CoordT]: @@ -359,6 +374,21 @@ def identity(self) -> CurvePoint[Curve[CoordT], CoordT]: def generator_point(self) -> CurvePoint[Curve[CoordT], CoordT]: return self.point_type.generator_point(self.curve) + def public_key_from_secret(self, secret_key: bytes) -> bytes: + if not isinstance(secret_key, bytes | bytearray): + raise TypeError("secret_key must be bytes") + secret_scalar = int.from_bytes(secret_key, "little") + return (self.generator_point() * secret_scalar).point_to_string() + + def secret_from_seed(self, seed: bytes) -> tuple[bytes, bytes]: + if not isinstance(seed, bytes | bytearray): + raise TypeError("seed must be bytes") + from dot_ring.vrf.transcript import scalar_encode, secret_from_seed_scalar + + secret_scalar = secret_from_seed_scalar(self, bytes(seed)) + secret_key = scalar_encode(self, secret_scalar) + return self.public_key_from_secret(secret_key), secret_key + def string_to_point(self, data: str | bytes) -> CurvePoint[Curve[CoordT], CoordT]: return self.point_type.string_to_point(data, self.curve) diff --git a/dot_ring/keygen.py b/dot_ring/keygen.py deleted file mode 100644 index d851bb1..0000000 --- a/dot_ring/keygen.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -from dot_ring.curve.curve import CurveVariant -from dot_ring.curve.specs.bandersnatch import Bandersnatch -from dot_ring.vrf.ietf import TinyVRF -from dot_ring.vrf.transcript import scalar_encode, secret_from_seed_scalar - - -def secret_from_seed(seed: bytes, curve: CurveVariant = Bandersnatch) -> tuple[bytes, bytes]: - """ - Deterministically derive a secret scalar and public key from a seed. - - Returns: - (public_key_bytes, secret_scalar_bytes) - """ - if not isinstance(seed, (bytes, bytearray)): - raise TypeError("seed must be bytes") - if not isinstance(curve, CurveVariant): - raise TypeError("curve must be a CurveVariant") - - sk_int = secret_from_seed_scalar(curve, bytes(seed)) - sk_bytes = scalar_encode(curve, sk_int) - pk_bytes = TinyVRF[curve].get_public_key(sk_bytes) - return pk_bytes, sk_bytes diff --git a/dot_ring/ring_proof/columns/columns.py b/dot_ring/ring_proof/columns/columns.py index 4eca673..dd877cb 100644 --- a/dot_ring/ring_proof/columns/columns.py +++ b/dot_ring/ring_proof/columns/columns.py @@ -2,10 +2,10 @@ import secrets from collections.abc import Sequence -from dataclasses import dataclass -from typing import cast +from dataclasses import dataclass, field -from dot_ring.ring_proof.constants import DEFAULT_SIZE, MAX_RING_SIZE, OMEGAS, S_PRIME, ZK_ROWS, SeedPoint +from dot_ring.curve.point import CurvePoint +from dot_ring.ring_proof.constants import DEFAULT_SIZE, MAX_RING_SIZE, OMEGAS, S_PRIME, ZK_ROWS from dot_ring.ring_proof.params import RingProofParams from dot_ring.ring_proof.pcs.kzg import KZG from dot_ring.ring_proof.pcs.protocol import PCS, G1Commitment @@ -67,15 +67,13 @@ class WitnessColumnBuilder: selector_vector: list[int] producer_index: int secret_t: int + params: RingProofParams = field(default_factory=RingProofParams, repr=False) size: int = DEFAULT_SIZE omega: int = OMEGAS[DEFAULT_SIZE] prime: int = S_PRIME max_ring_size: int = MAX_RING_SIZE padding_rows: int = 4 test_vectors: bool = False - seed_point: tuple[int, int] = SeedPoint - params: RingProofParams | None = None - pcs: type[PCS] = KZG @classmethod def from_params( @@ -91,29 +89,15 @@ def from_params( selector_vector=selector_vector, producer_index=producer_index, secret_t=secret_t, + params=params, size=params.domain_size, omega=params.omega, prime=params.prime, max_ring_size=params.max_ring_size, padding_rows=params.padding_rows, test_vectors=params.test_vectors, - seed_point=params.seed_point, - params=params, - pcs=params.pcs, ) - def _add(self, point1: tuple[int, int], point2: tuple[int, int]) -> tuple[int, int]: - if self.params is None: - raise ValueError("Ring proof params are required for point arithmetic") - result = self.params.add_points(point1, point2) - return result - - def _mul(self, scalar: int, point: tuple[int, int]) -> tuple[int, int]: - if self.params is None: - raise ValueError("Ring proof params are required for point arithmetic") - result = self.params.mul_point(scalar, point) - return result - def _bits_vector(self) -> list[int]: bv = [1 if i == self.producer_index else 0 for i in range(self.max_ring_size)] t_bits = bin(self.secret_t)[2:][::-1] @@ -131,12 +115,16 @@ def _bits_vector(self) -> list[int]: return bv def _conditional_sum_accumulator(self, b_vector: list[int]) -> tuple[list[int], list[int]]: - seed_sw = self.seed_point - - acc = [seed_sw] + acc = [self.params.cv.curve.params.auxiliary_points.accumulator_base] acc_len = self.size - self.padding_rows + 1 for i in range(1, acc_len): - next_pt = acc[i - 1] if b_vector[i - 1] == 0 else self._add(acc[i - 1], self.ring_pk[i - 1]) + if b_vector[i - 1] == 0: + next_pt = acc[i - 1] + else: + current = acc[i - 1] + member = self.ring_pk[i - 1] + added = self.params.cv.point(current[0], current[1]) + self.params.cv.point(member[0], member[1]) + next_pt = int(added.x), int(added.y) acc.append(next_pt) return [point[0] for point in acc], [point[1] for point in acc] @@ -160,25 +148,10 @@ def build(self) -> tuple[Column, Column, Column, Column]: ] for col in columns: col.interpolate(self.omega, self.prime, hidden=True, test_vectors=self.test_vectors) - col.commit(self.pcs) + col.commit(self.params.pcs) return (columns[0], columns[1], columns[2], columns[3]) - def result(self, Blinding_point: tuple[int, int]) -> tuple[int, int]: - """ - input: public key, secret vector and Blinding Base - output: relation as Result Point - """ - # sw_H = sw.from_twisted_edwards(Blinding_point) - sw_H = Blinding_point - PK_k = self.ring_pk[self.producer_index] - Result_point = cast( - tuple[int, int], - self._add(PK_k, self._mul(self.secret_t, sw_H)), - ) - return Result_point - - def result_p_seed(self, result: tuple[int, int]) -> tuple[int, int]: - """result plus seed""" - # res=sw.add(result, sw.from_twisted_edwards(SeedPoint)) - res = cast(tuple[int, int], self._add(result, self.seed_point)) - return res + def result(self, blinding_point: tuple[int, int]) -> CurvePoint: + producer_point = self.params.cv.point(self.ring_pk[self.producer_index]) + blinded = self.params.cv.point(blinding_point) * self.secret_t + return producer_point + blinded diff --git a/dot_ring/ring_proof/constraints/constraints.py b/dot_ring/ring_proof/constraints/constraints.py index 0aefea4..fdbe9b4 100644 --- a/dot_ring/ring_proof/constraints/constraints.py +++ b/dot_ring/ring_proof/constraints/constraints.py @@ -2,9 +2,7 @@ from collections.abc import Sequence from dataclasses import dataclass, field -from typing import Any, cast -from dot_ring.ring_proof.constants import SeedPoint from dot_ring.ring_proof.params import RingProofParams from dot_ring.ring_proof.polynomial.fft import evaluate_poly_fft from dot_ring.ring_proof.polynomial.ops import lagrange_basis_polynomial @@ -12,14 +10,14 @@ def _to_radix(vec: Sequence[int], radix_domain_size: int, radix_omega: int, prime: int) -> list[int]: """Convert a radix‑2 evaluation vector to radix‑domain evaluations.""" - return cast(list[int], evaluate_poly_fft(list(vec), radix_domain_size, radix_omega, prime)) + return evaluate_poly_fft(list(vec), radix_domain_size, radix_omega, prime) @dataclass(slots=True) class RingConstraintBuilder: """Compute c₁ₓ … c₇ₓ evaluation vectors from column evaluations.""" - Result_plus_Seed: Any + result_plus_seed: tuple[int, int] # radix‑2 evaluation vectors coming from Column objects px: Sequence[int] py: Sequence[int] @@ -29,7 +27,6 @@ class RingConstraintBuilder: acc_y: Sequence[int] acc_ip: Sequence[int] params: RingProofParams = field(default_factory=RingProofParams, repr=False) - seed_point: tuple[int, int] = SeedPoint _px4: list[int] = field(init=False, repr=False) _py4: list[int] = field(init=False, repr=False) @@ -38,24 +35,24 @@ class RingConstraintBuilder: _accx4: list[int] = field(init=False, repr=False) _accy4: list[int] = field(init=False, repr=False) _accip4: list[int] = field(init=False, repr=False) - _ctx: dict[str, Any] = field(init=False, repr=False) + _shift: int = field(init=False, repr=False) + _not_last: list[int] = field(init=False, repr=False) + _l0_radix: list[int] = field(init=False, repr=False) + _ln_radix: list[int] = field(init=False, repr=False) def __post_init__(self) -> None: domain = self.params.domain radix_domain = self.params.radix_domain last_index = self.params.last_index last_root = pow(self.params.omega, last_index, self.params.prime) - not_last = [(int(x) - last_root) % self.params.prime for x in radix_domain] l0_coeffs = lagrange_basis_polynomial(domain, 0, self.params.prime) ln_coeffs = lagrange_basis_polynomial(domain, last_index, self.params.prime) - self._ctx = { - "radix_domain": radix_domain, - "shift": self.params.radix_shift, - "not_last": not_last, - "l0_radix": cast(list[int], evaluate_poly_fft(l0_coeffs, self.params.radix_domain_size, self.params.radix_omega, self.params.prime)), - "ln_radix": cast(list[int], evaluate_poly_fft(ln_coeffs, self.params.radix_domain_size, self.params.radix_omega, self.params.prime)), - } - radix_domain = self._ctx["radix_domain"] + + self._shift = self.params.radix_shift + self._not_last = [(x - last_root) % self.params.prime for x in radix_domain] + self._l0_radix = evaluate_poly_fft(l0_coeffs, len(radix_domain), self.params.radix_omega, self.params.prime) + self._ln_radix = evaluate_poly_fft(ln_coeffs, len(radix_domain), self.params.radix_omega, self.params.prime) + self._px4 = _to_radix(self.px, len(radix_domain), self.params.radix_omega, self.params.prime) self._py4 = _to_radix(self.py, len(radix_domain), self.params.radix_omega, self.params.prime) self._s4 = _to_radix(self.s, len(radix_domain), self.params.radix_omega, self.params.prime) @@ -78,7 +75,7 @@ def compute(self) -> dict[str, list[int]]: } def _shifted_index(self, index: int) -> int: - shifted = index + self._ctx["shift"] + shifted = index + self._shift if shifted >= len(self._px4): shifted -= len(self._px4) return shifted @@ -86,9 +83,8 @@ def _shifted_index(self, index: int) -> int: def _c1(self) -> list[int]: """Inner-product accumulator transition.""" prime = self.params.prime - not_last = self._ctx["not_last"] result: list[int] = [] - for i, (accip_i, b_i, s_i, not_last_i) in enumerate(zip(self._accip4, self._b4, self._s4, not_last, strict=True)): + for i, (accip_i, b_i, s_i, not_last_i) in enumerate(zip(self._accip4, self._b4, self._s4, self._not_last, strict=True)): accip_w = self._accip4[self._shifted_index(i)] result.append(((accip_w - accip_i - b_i * s_i) * not_last_i) % prime) return result @@ -96,8 +92,7 @@ def _c1(self) -> list[int]: def _c2_c3(self) -> tuple[list[int], list[int]]: """Twisted-Edwards x/y accumulator transition constraints.""" prime = self.params.prime - te_coeff_a = self.params.ring_edwards_a - not_last = self._ctx["not_last"] + te_coeff_a = self.params.cv.curve.params.a c2: list[int] = [] c3: list[int] = [] @@ -110,7 +105,7 @@ def _c2_c3(self) -> tuple[list[int], list[int]]: x3 = self._accx4[shifted] y3 = self._accy4[shifted] b_i = self._b4[i] - not_last_i = not_last[i] + not_last_i = self._not_last[i] one_minus_b = (1 - b_i) % prime x1_y1 = (x1 * y1) % prime @@ -136,14 +131,12 @@ def _c4(self) -> list[int]: def _c5_c6(self) -> tuple[list[int], list[int]]: """Accumulator boundary constraints for x/y coordinates.""" prime = self.params.prime - seed_x, seed_y = self.seed_point - result_x, result_y = self.Result_plus_Seed - l0_radix = self._ctx["l0_radix"] - ln_radix = self._ctx["ln_radix"] + seed_x, seed_y = self.params.cv.curve.params.auxiliary_points.accumulator_base + result_x, result_y = self.result_plus_seed c5: list[int] = [] c6: list[int] = [] - for accx_i, accy_i, l0_i, ln_i in zip(self._accx4, self._accy4, l0_radix, ln_radix, strict=True): + for accx_i, accy_i, l0_i, ln_i in zip(self._accx4, self._accy4, self._l0_radix, self._ln_radix, strict=True): c5.append(((accx_i - seed_x) * l0_i + (accx_i - result_x) * ln_i) % prime) c6.append(((accy_i - seed_y) * l0_i + (accy_i - result_y) * ln_i) % prime) @@ -152,10 +145,8 @@ def _c5_c6(self) -> tuple[list[int], list[int]]: def _c7(self) -> list[int]: """Inner-product accumulator boundary constraint.""" prime = self.params.prime - l0_radix = self._ctx["l0_radix"] - ln_radix = self._ctx["ln_radix"] result: list[int] = [] - for accip_i, l0_i, ln_i in zip(self._accip4, l0_radix, ln_radix, strict=True): + for accip_i, l0_i, ln_i in zip(self._accip4, self._l0_radix, self._ln_radix, strict=True): result.append((accip_i * l0_i + (accip_i - 1) * ln_i) % prime) return result diff --git a/dot_ring/ring_proof/params.py b/dot_ring/ring_proof/params.py index 00becff..2e40026 100644 --- a/dot_ring/ring_proof/params.py +++ b/dot_ring/ring_proof/params.py @@ -4,8 +4,8 @@ from functools import lru_cache from dot_ring.curve.curve import CurveVariant -from dot_ring.curve.point import CurvePoint from dot_ring.curve.specs.bandersnatch import Bandersnatch +from dot_ring.curve.twisted_edwards.te_curve import TECurve from dot_ring.ring_proof.constants import ( D_2048, DEFAULT_SIZE, @@ -50,23 +50,6 @@ def _uses_precomputed_bls_domain(prime: int, base_root: int, base_size: int) -> return prime == S_PRIME and base_root == OMEGA_2048 and base_size == 2048 -# Global bounded cache: roots are deterministic for one size/field pair. -@lru_cache(maxsize=16) -def _primitive_root_of_unity(size: int, prime: int) -> int: - if not _is_power_of_two(size): - raise ValueError(f"root size must be a power of two, got {size}") - if (prime - 1) % size != 0: - raise ValueError(f"root size {size} does not divide prime - 1") - exponent = (prime - 1) // size - candidate = 2 - while True: - root = pow(candidate, exponent, prime) - if root != 1 and pow(root, size, prime) == 1 and pow(root, size // 2, prime) != 1: - return root - candidate += 1 - - -# Global bounded cache: evaluation domains are reused across roots/proofs. @lru_cache(maxsize=32) def _domain_for_size( domain_size: int, @@ -128,20 +111,6 @@ def _sqrt_mod_prime(n: int, prime: int) -> int: return x -_BANDERSNATCH_MONT_A_OVER_THREE = 9992940898322946442093665462003920523391277922024982836398934612730118446984 -_BANDERSNATCH_MONT_B = 25465760566081946422412445027709227188579564747101592991722834452325077642517 - - -def _bandersnatch_sw_to_te(point: tuple[int, int]) -> tuple[int, int]: - prime = S_PRIME - sw_x, sw_y = point - mont_x = (_BANDERSNATCH_MONT_B * sw_x - _BANDERSNATCH_MONT_A_OVER_THREE) % prime - mont_y = (_BANDERSNATCH_MONT_B * sw_y) % prime - v = mont_x * pow(mont_y, -1, prime) - w = (mont_x - 1) * pow((mont_x + 1) % prime, -1, prime) - return v % prime, w % prime - - # Global bounded cache: extended roots are deterministic for one target size. @lru_cache(maxsize=8) def _extend_root_to_size(base_root: int, base_size: int, target_size: int, prime: int) -> tuple[int, int]: @@ -154,7 +123,7 @@ def _extend_root_to_size(base_root: int, base_size: int, target_size: int, prime return root, size -@dataclass(frozen=True) +@dataclass class RingProofParams: domain_size: int = DEFAULT_SIZE max_ring_size: int = MAX_RING_SIZE @@ -165,7 +134,7 @@ class RingProofParams: base_root_size: int = 2048 pcs: type[PCS] = field(default=KZG, compare=False, hash=False, repr=False) test_vectors: bool = False - cv: CurveVariant = field(default_factory=lambda: Bandersnatch, compare=False, hash=False) + cv: CurveVariant[int] = field(default_factory=lambda: Bandersnatch, compare=False, hash=False) @property def scalar_bits(self) -> int: @@ -176,12 +145,31 @@ def row_overhead(self) -> int: return self.scalar_bits + self.padding_rows def __post_init__(self) -> None: - if self.cv.name == "BabyJubJub": - raise ValueError("BabyJubJub ring proofs are not supported; use Tiny/Thin/Pedersen VRFs for that curve") + self._validate_curve() + radix_domain_size = self._init_radix_domain_size() + self._validate_domain_sizes(radix_domain_size) + self._configure_base_root(radix_domain_size) + self._validate_ring_capacity() + + def _validate_curve(self) -> None: + if not isinstance(self.cv.curve, TECurve): + raise ValueError(f"{self.cv.name} ring proofs require a Twisted Edwards curve") + if self.cv.curve.params.field_modulus != self.prime: + raise ValueError(f"{self.cv.name} ring proofs require field modulus {self.prime}") + + auxiliary_points = self.cv.curve.params.auxiliary_points + for name in ("blinding_base", "accumulator_base", "padding_point"): + if getattr(auxiliary_points, name) is None: + raise ValueError(f"{self.cv.name} ring proofs require auxiliary point {name}") + + def _init_radix_domain_size(self) -> int: radix_domain_size = self.radix_domain_size if radix_domain_size is None: radix_domain_size = self.domain_size * 4 - object.__setattr__(self, "radix_domain_size", radix_domain_size) + self.radix_domain_size = radix_domain_size + return radix_domain_size + + def _validate_domain_sizes(self, radix_domain_size: int) -> None: if not _is_power_of_two(self.domain_size): raise ValueError(f"domain_size must be a power of two, got {self.domain_size}") if not _is_power_of_two(radix_domain_size): @@ -190,12 +178,16 @@ def __post_init__(self) -> None: raise ValueError(f"domain_size {self.domain_size} must divide radix_domain_size {radix_domain_size}") if self.domain_size > MAX_PIOP_DOMAIN_SIZE: raise ValueError(f"domain_size {self.domain_size} exceeds supported SRS domain size {MAX_PIOP_DOMAIN_SIZE}") + + def _configure_base_root(self, radix_domain_size: int) -> None: if radix_domain_size > self.base_root_size: root, size = _extend_root_to_size(self.base_root, self.base_root_size, radix_domain_size, self.prime) - object.__setattr__(self, "base_root", root) - object.__setattr__(self, "base_root_size", size) + self.base_root = root + self.base_root_size = size if self.base_root_size % radix_domain_size != 0: raise ValueError(f"radix_domain_size {radix_domain_size} must divide base_root_size {self.base_root_size}") + + def _validate_ring_capacity(self) -> None: if self.padding_rows < 1: raise ValueError("padding_rows must be >= 1 to preserve accumulator structure") if self.padding_rows >= self.domain_size: @@ -209,7 +201,7 @@ def __post_init__(self) -> None: f"domain_size={self.domain_size}, scalar_bits={self.scalar_bits}, padding_rows={self.padding_rows}" ) if self.max_ring_size == MAX_RING_SIZE and max_supported != MAX_RING_SIZE: - object.__setattr__(self, "max_ring_size", max_supported) + self.max_ring_size = max_supported elif self.max_ring_size > max_supported: raise ValueError(f"max_ring_size {self.max_ring_size} exceeds supported size {max_supported}") @@ -254,65 +246,6 @@ def max_effective_ring_size(self) -> int: def required_srs_degree(self) -> int: return max(self.domain_size - 1, self._radix_domain_size - self.domain_size) - @property - def blinding_base(self) -> tuple[int, int]: - base = self.cv.curve.params.auxiliary_points.blinding_base - if base is None: - raise ValueError(f"{self.cv.name} does not define a ring blinding base") - return self.affine_to_ring_point((int(base[0]), int(base[1]))) - - @property - def seed_point(self) -> tuple[int, int]: - base = self.cv.curve.params.auxiliary_points.accumulator_base - if base is None: - raise ValueError(f"{self.cv.name} does not define a ring accumulator base") - return self.affine_to_ring_point((int(base[0]), int(base[1]))) - - @property - def padding_point(self) -> tuple[int, int]: - point = self.cv.curve.params.auxiliary_points.padding_point - if point is None: - raise ValueError(f"{self.cv.name} does not define a ring padding point") - return self.affine_to_ring_point((int(point[0]), int(point[1]))) - - def affine_to_ring_point(self, point: tuple[int, int]) -> tuple[int, int]: - if self.cv.name == "Bandersnatch_SW": - return _bandersnatch_sw_to_te(point) - return point - - def point_to_ring_point(self, point: CurvePoint) -> tuple[int, int]: - return self.affine_to_ring_point((int(point.x), int(point.y))) - - def ring_curve_variant(self) -> CurveVariant: - if self.cv.name == "Bandersnatch_SW": - return Bandersnatch - return self.cv - - def ring_point(self, point: tuple[int, int]) -> CurvePoint: - return self.ring_curve_variant().point(point[0], point[1]) - - @property - def ring_edwards_a(self) -> int: - if self.cv.name == "Bandersnatch_SW": - return int(Bandersnatch.curve.params.a) - return int(self.cv.curve.params.a) - - @property - def ring_edwards_d(self) -> int: - if self.cv.name == "Bandersnatch_SW": - return int(Bandersnatch.curve.params.d) - return int(self.cv.curve.params.d) - - def add_points(self, point1: tuple[int, int], point2: tuple[int, int]) -> tuple[int, int]: - p1 = self.ring_point(point1) - p2 = self.ring_point(point2) - result = p1 + p2 - return int(result.x), int(result.y) - - def mul_point(self, scalar: int, point: tuple[int, int]) -> tuple[int, int]: - result = self.ring_point(point) * scalar - return int(result.x), int(result.y) - @classmethod def from_ring_size( cls, @@ -322,7 +255,7 @@ def from_ring_size( base_root: int = OMEGA_2048, base_root_size: int = 2048, test_vectors: bool = False, - cv: CurveVariant = Bandersnatch, + cv: CurveVariant[int] = Bandersnatch, ) -> RingProofParams: """ Automatically construct RingProofParams based on ring size. diff --git a/dot_ring/ring_proof/ring_curve.py b/dot_ring/ring_proof/ring_curve.py new file mode 100644 index 0000000..a676fd3 --- /dev/null +++ b/dot_ring/ring_proof/ring_curve.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from typing import Any, cast + +from dot_ring.curve.curve import CurveVariant +from dot_ring.curve.point import CurvePoint +from dot_ring.curve.specs.bandersnatch import Bandersnatch +from dot_ring.curve.twisted_edwards.te_curve import TECurve + +_BANDERSNATCH_MONT_A_OVER_THREE = 9992940898322946442093665462003920523391277922024982836398934612730118446984 +_BANDERSNATCH_MONT_B = 25465760566081946422412445027709227188579564747101592991722834452325077642517 + + +def _bandersnatch_sw_to_te(point: tuple[int, int]) -> tuple[int, int]: + prime = Bandersnatch.curve.params.field_modulus + sw_x, sw_y = point + mont_x = (_BANDERSNATCH_MONT_B * sw_x - _BANDERSNATCH_MONT_A_OVER_THREE) % prime + mont_y = (_BANDERSNATCH_MONT_B * sw_y) % prime + v = mont_x * pow(mont_y, -1, prime) + w = (mont_x - 1) * pow((mont_x + 1) % prime, -1, prime) + return v % prime, w % prime + + +def ring_proof_curve(public_curve: CurveVariant) -> CurveVariant: + if public_curve.name == "Bandersnatch_SW": + return Bandersnatch + return public_curve + + +def ring_coords(public_curve: CurveVariant, point: CurvePoint | tuple[int, int]) -> tuple[int, int]: + if isinstance(point, tuple): + coords = int(point[0]), int(point[1]) + else: + coords = int(cast(int, point.x)), int(cast(int, point.y)) + if public_curve.name == "Bandersnatch_SW": + return _bandersnatch_sw_to_te(coords) + return coords + + + +def validate_ring_curve(public_curve: CurveVariant, prime: int) -> None: + curve = ring_proof_curve(public_curve) + if not isinstance(curve.curve, TECurve): + raise ValueError(f"{public_curve.name} ring proofs require a Twisted Edwards ring curve") + if curve.curve.params.field_modulus != prime: + raise ValueError(f"{public_curve.name} ring proofs require field modulus {prime}") + for name in ("blinding_base", "accumulator_base", "padding_point"): + ring_auxiliary_point(public_curve, name) diff --git a/dot_ring/ring_proof/transcript/phases.py b/dot_ring/ring_proof/transcript/phases.py index 259f758..107a628 100644 --- a/dot_ring/ring_proof/transcript/phases.py +++ b/dot_ring/ring_proof/transcript/phases.py @@ -3,6 +3,7 @@ from collections.abc import Sequence from typing import Any +from dot_ring.curve.point import CurvePoint from dot_ring.ring_proof.transcript.transcript import FiatShamirTranscript __all__ = [ @@ -16,7 +17,7 @@ def phase1_alphas_after_vk( t: FiatShamirTranscript, - result_point: Any, + result_point: CurvePoint, witness_commitments: Sequence[Any], ) -> Any: """Append phase-1 data after the verifier key has already been absorbed.""" @@ -44,7 +45,7 @@ def phase3_nu_vector( def derive_challenges_after_vk( t: FiatShamirTranscript, - result_point: Any, + result_point: CurvePoint, witness_commitments: Sequence[Any], quotient_commitment: Any, evals: list[int] | bytes, @@ -73,11 +74,11 @@ def serialize_verifier_key(g1: Any, g2_points: Sequence[Any], commitments: Seque return _serialize_bls_pair(g1) + b"".join(_serialize_bls_pair(point) for point in g2_points) + _serialize_commitments(commitments) -def _serialize_instance(result_point: Any) -> bytes: - if isinstance(result_point, tuple) and len(result_point) == 2: - x, y = result_point - return int(x).to_bytes(32, "little") + int(y).to_bytes(32, "little") - return _serialize_bytes(result_point) +def _serialize_instance(result_point: CurvePoint) -> bytes: + x, y = result_point.x, result_point.y + if x is None or y is None: + raise ValueError("Cannot serialize identity point") + return int(x).to_bytes(32, "little") + int(y).to_bytes(32, "little") def _serialize_commitments(commitments: Sequence[Any]) -> bytes: diff --git a/dot_ring/ring_proof/verify.py b/dot_ring/ring_proof/verify.py index 936bd56..11137cc 100644 --- a/dot_ring/ring_proof/verify.py +++ b/dot_ring/ring_proof/verify.py @@ -1,5 +1,6 @@ from typing import Any, NamedTuple +from dot_ring.curve.point import CurvePoint from dot_ring.ring_proof.constants import OMEGA_2048, S_PRIME from dot_ring.ring_proof.pcs.kzg import KZG from dot_ring.ring_proof.pcs.protocol import PCS @@ -53,6 +54,13 @@ def _ring_proof_fields(proof: RingProofFields | tuple[Any, ...]) -> RingProofFie return RingProofFields(*proof) +def _point_coords(point: CurvePoint) -> tuple[int, int]: + x, y = point.x, point.y + if x is None or y is None: + raise ValueError("identity point is not a valid ring proof instance") + return int(x), int(y) + + def _compute_quotient_and_linearization_terms( alpha_list: list[int], v_list: list[int], @@ -176,12 +184,12 @@ def _compute_quotient_and_linearization_terms( return agg_zeta, scalar_accip, scalar_accx, scalar_accy, zeta_omega, l_zeta_omega -def prepare_linear_pcs_verifications_fast( +def linear_pcs_verifications( proof: RingProofFields | tuple[Any, ...], fixed_cols_blst: tuple[Any, Any, Any], - relation_to_prove: tuple[int, int], - result_plus_seed: tuple[int, int], - seed_point: tuple[int, int], + relation_to_prove: CurvePoint, + result_plus_seed: CurvePoint, + seed_point: CurvePoint, domain: list[int], padding_rows: int, domain_size_inv: int, @@ -192,6 +200,8 @@ def prepare_linear_pcs_verifications_fast( quotient_commitment: bytes, ) -> tuple[LinearPcsVerification, LinearPcsVerification]: proof_fields = _ring_proof_fields(proof) + result_plus_seed_coords = _point_coords(result_plus_seed) + seed_coords = _point_coords(seed_point) _, alpha_list, zeta_p, v_list = derive_challenges_after_vk( transcript_prefix, @@ -215,10 +225,10 @@ def prepare_linear_pcs_verifications_fast( proof_fields.accx_zeta, proof_fields.accy_zeta, proof_fields.l_zeta_omega, - seed_point[0], - seed_point[1], - result_plus_seed[0], - result_plus_seed[1], + seed_coords[0], + seed_coords[1], + result_plus_seed_coords[0], + result_plus_seed_coords[1], domain[last_index], domain[-1], domain[-2], @@ -270,9 +280,9 @@ def __init__( self, proof: RingProofFields | tuple[Any, ...], fixed_cols: list, - relation_to_prove: tuple | bytes, - result_plus_seed: tuple, - seed_point: tuple, + relation_to_prove: CurvePoint, + result_plus_seed: CurvePoint, + seed_point: CurvePoint, domain: list, transcript_prefix: FiatShamirTranscript, padding_rows: int = 4, @@ -303,7 +313,9 @@ def __init__( self.Cpx, self.Cpy, self.Cs = fixed_cols self.relation_to_prove = relation_to_prove - self.Result_plus_Seed, self.sp, self.D = result_plus_seed, seed_point, domain + self.Result_plus_Seed = _point_coords(result_plus_seed) + self.sp = _point_coords(seed_point) + self.D = domain self.padding_rows = padding_rows self.prime = prime self.omega = omega @@ -356,9 +368,9 @@ def _domain_omega(self) -> int: def is_valid(self) -> bool: """If both the verifications are true then sign is valid""" - return bool(self.pcs.batch_verify_linear_preconverted(list(self._prepare_linear_pcs_verifications()))) + return bool(self.pcs.batch_verify_linear_preconverted(list(self._linear_pcs_verifications()))) - def _prepare_linear_pcs_verifications( + def _linear_pcs_verifications( self, ) -> tuple[LinearPcsVerification, LinearPcsVerification]: """Prepare deferred linear KZG equations from one native scalar pass.""" diff --git a/dot_ring/vrf/ietf/thin.py b/dot_ring/vrf/ietf/thin.py index 709b610..19dfcdc 100644 --- a/dot_ring/vrf/ietf/thin.py +++ b/dot_ring/vrf/ietf/thin.py @@ -42,7 +42,7 @@ class ThinVRF(VRF[Any]): s: int @classmethod - def from_bytes(cls, proof_bytes: bytes) -> ThinVRF: + def decode(cls, proof_bytes: bytes) -> ThinVRF: encoded_point_len = point_len(cls.cv) scalar_size = scalar_len(cls.cv) expected = 2 * encoded_point_len + scalar_size @@ -58,7 +58,7 @@ def from_bytes(cls, proof_bytes: bytes) -> ThinVRF: raise ValueError("Response scalar s is not less than the curve order") return cls(output_point, r, s) - def to_bytes(self) -> bytes: + def encode(self) -> bytes: return self.output_point.point_to_string() + self.r.point_to_string() + scalar_encode(self.cv, self.s) @classmethod diff --git a/dot_ring/vrf/ietf/tiny.py b/dot_ring/vrf/ietf/tiny.py index 9bb0d82..6b1d8bc 100644 --- a/dot_ring/vrf/ietf/tiny.py +++ b/dot_ring/vrf/ietf/tiny.py @@ -20,7 +20,7 @@ vrf_transcript_scalars, ) -from ..vrf import VRF, PreparedSecretKey +from ..vrf import VRF @dataclass @@ -43,7 +43,7 @@ class TinyVRF(VRF[Any]): s: int @classmethod - def from_bytes(cls, proof_bytes: bytes) -> TinyVRF: + def decode(cls, proof_bytes: bytes) -> TinyVRF: encoded_point_len = point_len(cls.cv) scalar_size = scalar_len(cls.cv) expected = encoded_point_len + CHALLENGE_LEN + scalar_size @@ -60,7 +60,7 @@ def from_bytes(cls, proof_bytes: bytes) -> TinyVRF: raise ValueError("Response scalar s is not less than the curve order") return cls(output_point, c, s) - def to_bytes(self) -> bytes: + def encode(self) -> bytes: return self.output_point.point_to_string() + self.c.to_bytes(CHALLENGE_LEN, "little") + scalar_encode(self.cv, self.s) @classmethod @@ -77,20 +77,10 @@ def prove( additional_data: bytes, salt: bytes = b"", ) -> TinyVRF: - return cls.prove_prepared(alpha, cls.prepare_secret_key(secret_key), additional_data, salt) - - @classmethod - def prove_prepared( - cls, - alpha: bytes, - secret_key: PreparedSecretKey[Any], - additional_data: bytes, - salt: bytes = b"", - ) -> TinyVRF: - if secret_key.curve is not cls.cv: - raise ValueError("prepared secret key uses a different curve") - io = cls._io_from_alpha(alpha, secret_key.secret_scalar, salt) - return cls.prove_ios([io], secret_key.secret_scalar, secret_key.public_key, additional_data) + secret_scalar = scalar_decode(cls.cv, secret_key) + public_key = cls.cv.generator_point() * secret_scalar + io = cls._io_from_alpha(alpha, secret_scalar, salt) + return cls.prove_ios([io], secret_scalar, public_key, additional_data) @classmethod def prove_ios( diff --git a/dot_ring/vrf/io.py b/dot_ring/vrf/io.py index 89f8bad..f8b6fad 100644 --- a/dot_ring/vrf/io.py +++ b/dot_ring/vrf/io.py @@ -10,5 +10,5 @@ class VrfIo: input: CurvePoint output: CurvePoint - def to_bytes(self) -> bytes: + def encode(self) -> bytes: return self.input.point_to_string() + self.output.point_to_string() diff --git a/dot_ring/vrf/pedersen/__init__.py b/dot_ring/vrf/pedersen/__init__.py index cba4d38..9d25e5e 100644 --- a/dot_ring/vrf/pedersen/__init__.py +++ b/dot_ring/vrf/pedersen/__init__.py @@ -1,4 +1,4 @@ -from .batch_verifier import PedersenBatchItem, PedersenBatchVerifier +from .batch_verifier import PedersenBatchVerifier from .vrf import PedersenVRF -__all__ = ["PedersenBatchItem", "PedersenBatchVerifier", "PedersenVRF"] +__all__ = ["PedersenBatchVerifier", "PedersenVRF"] diff --git a/dot_ring/vrf/pedersen/batch_verifier.py b/dot_ring/vrf/pedersen/batch_verifier.py index ddceb13..6dc76de 100644 --- a/dot_ring/vrf/pedersen/batch_verifier.py +++ b/dot_ring/vrf/pedersen/batch_verifier.py @@ -11,7 +11,7 @@ @dataclass -class PedersenBatchItem: +class _PedersenBatchItem: c: int ios: list[VrfIo] zs: list[int] @@ -83,7 +83,7 @@ def _batch_coefficient_pairs_from_bytes(item_count: int, item_bytes: bytes | byt return pairs -def _batch_coefficient_item_bytes(item: PedersenBatchItem, scalar_size: int, order: int) -> bytes: +def _batch_coefficient_item_bytes(item: _PedersenBatchItem, scalar_size: int, order: int) -> bytes: return ( int(item.c % order).to_bytes(scalar_size, "little") + int(item.s % order).to_bytes(scalar_size, "little") @@ -91,7 +91,7 @@ def _batch_coefficient_item_bytes(item: PedersenBatchItem, scalar_size: int, ord ) -def _batch_coefficient_pairs(items: list[PedersenBatchItem], curve: CurveVariant) -> list[tuple[int, int]]: +def _batch_coefficient_pairs(items: list[_PedersenBatchItem], curve: CurveVariant) -> list[tuple[int, int]]: order = curve.curve.params.subgroup_order scalar_size = (order.bit_length() + 7) // 8 item_bytes = bytearray() @@ -103,7 +103,7 @@ def _batch_coefficient_pairs(items: list[PedersenBatchItem], curve: CurveVariant class PedersenBatchVerifier: def __init__(self, curve: CurveVariant): self.cv = curve - self.items: list[PedersenBatchItem] = [] + self.items: list[_PedersenBatchItem] = [] self._order = curve.curve.params.subgroup_order self._scalar_size = (self._order.bit_length() + 7) // 8 self._coefficient_item_bytes = bytearray() @@ -121,14 +121,14 @@ def __init__(self) -> None: _SpecializedPedersenBatchVerifier.__name__ = f"{cls.__name__}[{curve_variant.name}]" return _SpecializedPedersenBatchVerifier - def prepare(self, ios: list[VrfIo], additional_data: bytes, proof: PedersenVRF) -> PedersenBatchItem: + def _item(self, ios: list[VrfIo], additional_data: bytes, proof: PedersenVRF) -> _PedersenBatchItem: if len(ios) == 1: - return self._prepare_single(ios[0], additional_data, proof) + return self._single_io_item(ios[0], additional_data, proof) transcript, scalar_stream = vrf_transcript_scalars(self.cv, DomSep.PEDERSEN_VRF, ios, additional_data) transcript.absorb(proof.blinded_pk.point_to_string()) c = challenge(self.cv, [proof.result_point, proof.ok], transcript) - return PedersenBatchItem( + return _PedersenBatchItem( c=c, ios=list(ios), zs=scalar_stream.take(len(ios)), @@ -139,7 +139,7 @@ def prepare(self, ios: list[VrfIo], additional_data: bytes, proof: PedersenVRF) sb=proof.sb, ) - def _prepare_single(self, io: VrfIo, additional_data: bytes, proof: PedersenVRF) -> PedersenBatchItem: + def _single_io_item(self, io: VrfIo, additional_data: bytes, proof: PedersenVRF) -> _PedersenBatchItem: absorbed = bytearray(suite_id(self.cv)) absorbed.append(DomSep.PEDERSEN_VRF) absorbed.extend((1).to_bytes(8, "little")) @@ -152,7 +152,7 @@ def _prepare_single(self, io: VrfIo, additional_data: bytes, proof: PedersenVRF) absorbed.extend(proof.result_point.point_to_string()) absorbed.extend(proof.ok.point_to_string()) c = _challenge_scalar_from_absorbed(self.cv, bytes(absorbed)) - return PedersenBatchItem( + return _PedersenBatchItem( c=c, ios=[io], zs=[1], @@ -163,13 +163,13 @@ def _prepare_single(self, io: VrfIo, additional_data: bytes, proof: PedersenVRF) sb=proof.sb, ) - def push_prepared(self, item: PedersenBatchItem) -> None: + def _push_item(self, item: _PedersenBatchItem) -> None: self.items.append(item) self._single_io_items = self._single_io_items and len(item.ios) == 1 and len(item.zs) == 1 and item.zs[0] == 1 self._coefficient_item_bytes.extend(_batch_coefficient_item_bytes(item, self._scalar_size, self._order)) def push(self, ios: list[VrfIo], additional_data: bytes, proof: PedersenVRF) -> None: - self.push_prepared(self.prepare(ios, additional_data, proof)) + self._push_item(self._item(ios, additional_data, proof)) def verify(self) -> bool: if not self.items: diff --git a/dot_ring/vrf/pedersen/vrf.py b/dot_ring/vrf/pedersen/vrf.py index 270ee5f..dde04bf 100644 --- a/dot_ring/vrf/pedersen/vrf.py +++ b/dot_ring/vrf/pedersen/vrf.py @@ -18,7 +18,7 @@ vrf_transcript, ) -from ..vrf import VRF, PreparedSecretKey +from ..vrf import VRF C = TypeVar("C", bound=CurveVariant) @@ -59,7 +59,7 @@ def proof_len(cls) -> int: return 4 * point_length + 2 * scalar_len(cls.cv) @classmethod - def from_bytes(cls, proof: bytes) -> PedersenVRF: + def decode(cls, proof: bytes) -> PedersenVRF: point_length = point_len(cls.cv) scalar_size = scalar_len(cls.cv) expected = cls.proof_len() @@ -90,7 +90,7 @@ def from_bytes(cls, proof: bytes) -> PedersenVRF: sb=sb, ) - def to_bytes(self) -> bytes: + def encode(self) -> bytes: return ( self.output_point.point_to_string() + self.blinded_pk.point_to_string() @@ -114,20 +114,10 @@ def prove( additional_data: bytes, salt: bytes = b"", ) -> PedersenVRF: - return cls.prove_prepared(alpha, cls.prepare_secret_key(secret_key), additional_data, salt) - - @classmethod - def prove_prepared( - cls, - alpha: bytes, - secret_key: PreparedSecretKey[Any], - additional_data: bytes, - salt: bytes = b"", - ) -> PedersenVRF: - if secret_key.curve is not cls.cv: - raise ValueError("prepared secret key uses a different curve") - io = cls._io_from_alpha(alpha, secret_key.secret_scalar, salt) - return cls.prove_ios([io], secret_key.secret_scalar, secret_key.public_key, additional_data) + secret_scalar = scalar_decode(cls.cv, secret_key) + public_key = cls.cv.generator_point() * secret_scalar + io = cls._io_from_alpha(alpha, secret_scalar, salt) + return cls.prove_ios([io], secret_scalar, public_key, additional_data) @classmethod def prove_ios( diff --git a/dot_ring/vrf/ring/__init__.py b/dot_ring/vrf/ring/__init__.py index 8108c79..eb465dd 100644 --- a/dot_ring/vrf/ring/__init__.py +++ b/dot_ring/vrf/ring/__init__.py @@ -1,4 +1,4 @@ -from .batch_verifier import RingBatchContext, RingBatchItem, RingBatchVerifier +from .batch_verifier import RingBatchVerifier from .context import RingContext, RingRootBuilder from .members import Ring from .root import RingRoot @@ -6,8 +6,6 @@ __all__ = [ "Ring", - "RingBatchItem", - "RingBatchContext", "RingBatchVerifier", "RingContext", "RingRoot", diff --git a/dot_ring/vrf/ring/batch_verifier.py b/dot_ring/vrf/ring/batch_verifier.py index 856750a..90ac6ba 100644 --- a/dot_ring/vrf/ring/batch_verifier.py +++ b/dot_ring/vrf/ring/batch_verifier.py @@ -1,15 +1,16 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any +from dot_ring.curve.point import CurvePoint from dot_ring.ring_proof.pcs.utils import ( LinearPcsVerification, g1_to_blst, ) from dot_ring.ring_proof.transcript.transcript import FiatShamirTranscript -from dot_ring.ring_proof.verify import Verify, prepare_linear_pcs_verifications_fast -from dot_ring.vrf.pedersen.batch_verifier import PedersenBatchItem, PedersenBatchVerifier +from dot_ring.ring_proof.verify import Verify, linear_pcs_verifications +from dot_ring.vrf.pedersen.batch_verifier import PedersenBatchVerifier, _PedersenBatchItem from dot_ring.vrf.transcript import VrfIo from .members import Ring @@ -20,29 +21,29 @@ @dataclass -class RingBatchItem: +class _RingBatchItem: curve: Any - pedersen: PedersenBatchItem + pedersen: _PedersenBatchItem pcs: Any verifications: tuple[Any, Any] @dataclass(frozen=True) -class RingBatchContext: +class _RingBatchContext: ring: Ring ring_root: RingRoot fixed_cols_cmts: list[Any] fixed_cols_blst: tuple[Any, Any, Any] transcript_prefix: FiatShamirTranscript - seed_point: tuple[int, int] + seed_point: CurvePoint domain: list[int] padding_rows: int omega: int - ring_edwards_a: int + edwards_a: int domain_size_inv: int @classmethod - def from_ring(cls, ring: Ring, ring_root: RingRoot, *, validate_ring_root: bool = True) -> RingBatchContext: + def from_ring(cls, ring: Ring, ring_root: RingRoot, *, validate_ring_root: bool = True) -> _RingBatchContext: if validate_ring_root and not ring_root.matches_ring(ring): raise ValueError("ring root does not match ring") fixed_cols_cmts = ring_root.fixed_commitments(ring.params) @@ -54,11 +55,11 @@ def from_ring(cls, ring: Ring, ring_root: RingRoot, *, validate_ring_root: bool fixed_cols_cmts=fixed_cols_cmts, fixed_cols_blst=tuple(g1_to_blst(commitment) for commitment in fixed_cols_cmts), transcript_prefix=transcript_prefix, - seed_point=ring.params.seed_point, + seed_point=ring.params.cv.point(ring.params.cv.curve.params.auxiliary_points.accumulator_base), domain=domain, padding_rows=ring.params.padding_rows, omega=ring.params.omega, - ring_edwards_a=ring.params.ring_edwards_a, + edwards_a=ring.params.cv.curve.params.a, domain_size_inv=pow(ring.params.domain_size, -1, ring.params.prime), ) @@ -90,7 +91,7 @@ def ring_proof_verifier(self, proof: RingVRF, message: Any) -> Verify: self.domain, self.transcript_prefix, padding_rows=self.padding_rows, - edwards_a=self.ring_edwards_a, + edwards_a=self.edwards_a, prime=ring.params.prime, omega=self.omega, pcs=ring.params.pcs, @@ -105,7 +106,7 @@ def ring_pcs_verifications(self, proof: RingVRF, message: Any) -> tuple[LinearPc rltn, res_plus_seed = _proof_relation_points(proof, message, ring.params) witness_commitments, quotient_commitment = _proof_transcript_commitments(proof, ring.params) - return prepare_linear_pcs_verifications_fast( + return linear_pcs_verifications( proof.ring_proof_tuple(), self.fixed_cols_blst, rltn, @@ -114,37 +115,37 @@ def ring_pcs_verifications(self, proof: RingVRF, message: Any) -> tuple[LinearPc self.domain, self.padding_rows, self.domain_size_inv, - self.ring_edwards_a, + self.edwards_a, self.omega, self.transcript_prefix, witness_commitments, quotient_commitment, ) - def prepare_ios(self, proof: RingVRF, ios: list[VrfIo], ad: bytes) -> RingBatchItem: - return _prepare_ios_with_context(proof, ios, ad, self) + def _item_from_ios(self, proof: RingVRF, ios: list[VrfIo], ad: bytes) -> _RingBatchItem: + return _ring_batch_item(proof, ios, ad, self) - def prepare(self, proof: RingVRF, input: bytes, ad: bytes) -> RingBatchItem: + def _item(self, proof: RingVRF, input: bytes, ad: bytes) -> _RingBatchItem: pedersen_proof = proof.require_pedersen_proof() input_point = proof.cv.encode_to_curve(input) - return self.prepare_ios(proof, [VrfIo(input_point, pedersen_proof.output_point)], ad) + return self._item_from_ios(proof, [VrfIo(input_point, pedersen_proof.output_point)], ad) - def _prepare_pedersen_item(self, proof: RingVRF, ios: list[VrfIo], ad: bytes) -> Any: + def _pedersen_item(self, proof: RingVRF, ios: list[VrfIo], ad: bytes) -> Any: pedersen_proof = proof.require_pedersen_proof() if self.ring.params.cv.name != proof.cv.name: raise ValueError("proof curve does not match ring curve") - return PedersenBatchVerifier(proof.cv).prepare(ios, ad, pedersen_proof) + return PedersenBatchVerifier(proof.cv)._item(ios, ad, pedersen_proof) - def _prepare_pcs_item(self, proof: RingVRF, message: Any) -> tuple[Any, tuple[LinearPcsVerification, LinearPcsVerification]]: + def _pcs_item(self, proof: RingVRF, message: Any) -> tuple[Any, tuple[LinearPcsVerification, LinearPcsVerification]]: pcs = self.ring.params.pcs # Deferred linear KZG verification avoids materializing small commitments before batching. return pcs, self.ring_pcs_verifications(proof, message) -def _prepare_ios_with_context(proof: RingVRF, ios: list[VrfIo], ad: bytes, context: RingBatchContext) -> RingBatchItem: - pedersen_item = context._prepare_pedersen_item(proof, ios, ad) - pcs, verifications = context._prepare_pcs_item(proof, proof.require_pedersen_proof().blinded_pk) - return RingBatchItem(proof.cv, pedersen_item, pcs, verifications) +def _ring_batch_item(proof: RingVRF, ios: list[VrfIo], ad: bytes, context: _RingBatchContext) -> _RingBatchItem: + pedersen_item = context._pedersen_item(proof, ios, ad) + pcs, verifications = context._pcs_item(proof, proof.require_pedersen_proof().blinded_pk) + return _RingBatchItem(proof.cv, pedersen_item, pcs, verifications) def _proof_transcript_commitments(proof: RingVRF, params: Any) -> tuple[bytes, Any]: @@ -161,57 +162,24 @@ def _proof_transcript_commitments(proof: RingVRF, params: Any) -> tuple[bytes, A return witness_commitments, quotient_commitment -def _proof_relation_points(proof: RingVRF, message: Any, params: Any) -> tuple[tuple[int, int], tuple[int, int]]: - relation = params.point_to_ring_point(message) - result_plus_seed = params.add_points(params.seed_point, relation) - return relation, result_plus_seed +def _proof_relation_points(proof: RingVRF, message: Any, params: Any) -> tuple[CurvePoint, CurvePoint]: + relation = params.cv.point(message) + seed = params.cv.point(params.cv.curve.params.auxiliary_points.accumulator_base) + return relation, seed + relation class RingBatchVerifier: - def __init__(self, context: RingBatchContext | None = None) -> None: - self.items: list[RingBatchItem] = [] + def __init__(self) -> None: + self.items: list[_RingBatchItem] = [] self.pedersen_batches: dict[str, PedersenBatchVerifier] = {} self.pcs_batches: dict[int, tuple[Any, list[Any]]] = {} - self._contexts: dict[tuple[int, int], RingBatchContext] = {} - if context is not None: - self._contexts[context.key] = context + self._contexts: dict[tuple[int, int], _RingBatchContext] = {} self._invalid = False - @staticmethod - def prepare_context(ring: Ring, ring_root: RingRoot, *, validate_ring_root: bool = True) -> RingBatchContext: - return RingBatchContext.from_ring(ring, ring_root, validate_ring_root=validate_ring_root) - - @staticmethod - def prepare_ios( - proof: RingVRF, - ios: list[VrfIo], - ad: bytes, - ring: Ring, - ring_root: RingRoot, - *, - validate_ring_root: bool = True, - ) -> RingBatchItem: - context = RingBatchContext.from_ring(ring, ring_root, validate_ring_root=validate_ring_root) - return context.prepare_ios(proof, ios, ad) - - @staticmethod - def prepare(proof: RingVRF, input: bytes, ad: bytes, ring: Ring, ring_root: RingRoot, *, validate_ring_root: bool = True) -> RingBatchItem: - pedersen_proof = proof.require_pedersen_proof() - curve = proof.cv - input_point = curve.encode_to_curve(input) - return RingBatchVerifier.prepare_ios( - proof, - [VrfIo(input_point, pedersen_proof.output_point)], - ad, - ring, - ring_root, - validate_ring_root=validate_ring_root, - ) - - def push_prepared(self, item: RingBatchItem) -> None: + def _push_item(self, item: _RingBatchItem) -> None: self.items.append(item) pedersen_batch = self.pedersen_batches.setdefault(item.curve.name, PedersenBatchVerifier(item.curve)) - pedersen_batch.push_prepared(item.pedersen) + pedersen_batch._push_item(item.pedersen) _, verifications = self.pcs_batches.setdefault(id(item.pcs), (item.pcs, [])) verifications.extend(item.verifications) @@ -229,9 +197,9 @@ def push_ios( root_key = (id(ring), id(ring_root)) context = self._contexts.get(root_key) if context is None: - context = RingBatchContext.from_ring(ring, ring_root, validate_ring_root=validate_ring_root) + context = _RingBatchContext.from_ring(ring, ring_root, validate_ring_root=validate_ring_root) self._contexts[root_key] = context - self.push_prepared(context.prepare_ios(proof, ios, ad)) + self._push_item(context._item_from_ios(proof, ios, ad)) except (AssertionError, TypeError, ValueError): self._invalid = True @@ -249,9 +217,9 @@ def push( root_key = (id(ring), id(ring_root)) context = self._contexts.get(root_key) if context is None: - context = RingBatchContext.from_ring(ring, ring_root, validate_ring_root=validate_ring_root) + context = _RingBatchContext.from_ring(ring, ring_root, validate_ring_root=validate_ring_root) self._contexts[root_key] = context - self.push_prepared(context.prepare(proof, input, ad)) + self._push_item(context._item(proof, input, ad)) except (AssertionError, TypeError, ValueError): self._invalid = True @@ -266,7 +234,7 @@ def verify(self) -> bool: try: return all( - pcs.batch_verify_linear_preconverted(cast(list[LinearPcsVerification], verifications)) + pcs.batch_verify_linear_preconverted(verifications) for pcs, verifications in self.pcs_batches.values() ) except (AssertionError, TypeError, ValueError): diff --git a/dot_ring/vrf/ring/context.py b/dot_ring/vrf/ring/context.py index 5621351..77e9eae 100644 --- a/dot_ring/vrf/ring/context.py +++ b/dot_ring/vrf/ring/context.py @@ -33,10 +33,10 @@ def ring_root(self, ring: Ring | Sequence[bytes | str] | bytes) -> RingRoot: ring = self.ring(ring) return RingRoot.from_ring(ring, self.params) - def ring_root_from_bytes(self, commitment: RingRoot | bytes) -> RingRoot: + def decode_ring_root(self, commitment: RingRoot | bytes) -> RingRoot: if isinstance(commitment, RingRoot): return commitment - return RingRoot.from_bytes(commitment, self.params) + return RingRoot.decode(commitment, self.params) def ring_root_builder(self) -> RingRootBuilder: return RingRootBuilder(self) diff --git a/dot_ring/vrf/ring/members.py b/dot_ring/vrf/ring/members.py index a9ccd0a..50561fa 100644 --- a/dot_ring/vrf/ring/members.py +++ b/dot_ring/vrf/ring/members.py @@ -1,7 +1,6 @@ from __future__ import annotations from collections.abc import Sequence -from functools import lru_cache from dot_ring.curve.curve import CurveVariant from dot_ring.ring_proof.params import RingProofParams @@ -15,36 +14,6 @@ def parse_concatenated_keys(keys: bytes, cv: CurveVariant) -> list[bytes]: return [keys[encoded_point_len * i : encoded_point_len * (i + 1)] for i in range(len(keys) // encoded_point_len)] -def blinding_base_powers(params: RingProofParams, count: int) -> tuple[tuple[int, int], ...]: - def _bb_cache_key(params: RingProofParams) -> tuple[str, int, int, int, tuple[int, int]]: - return ( - params.ring_curve_variant().name, - params.prime, - params.ring_edwards_a, - params.ring_edwards_d, - params.blinding_base, - ) - - return _blinding_base_powers(params, _bb_cache_key(params), count) - - -@lru_cache(maxsize=16) -def _blinding_base_powers( - params: RingProofParams, - _cache_key: tuple[str, int, int, int, tuple[int, int]], - count: int, -) -> tuple[tuple[int, int], ...]: - """Return `[2^0 * H, 2^1 * H, ...]` in ring coordinates.""" - point = params.blinding_base - points = [] - for _ in range(count): - points.append(point) - point_obj = params.ring_point(point) - result = point_obj + point_obj - point = int(result.x), int(result.y) - return tuple(points) - - class Ring: nm_points: tuple[tuple[int, int], ...] params: RingProofParams @@ -61,20 +30,21 @@ def __init__(self, keys: Sequence[bytes | str], params: RingProofParams | None = params = RingProofParams.from_ring_size(len(keys)) self.params = params + padding_point = params.cv.curve.params.auxiliary_points.padding_point if len(keys) > params.max_ring_size: raise ValueError(f"ring size {len(keys)} exceeds max supported size {params.max_ring_size}") nm_points: list[tuple[int, int]] = [] for key in keys: - point = self._decode_key(key) + point = self._decode_key(key, padding_point) if point is None: - nm_points.append(params.padding_point) + nm_points.append(padding_point) continue nm_points.append(point) while len(nm_points) < params.max_ring_size: - nm_points.append(params.padding_point) + nm_points.append(padding_point) fill_count = params.domain_size - params.padding_rows - len(nm_points) if fill_count > 0: @@ -84,7 +54,7 @@ def __init__(self, keys: Sequence[bytes | str], params: RingProofParams | None = self.nm_points = tuple(nm_points) - def _decode_key(self, key: object) -> tuple[int, int] | None: + def _decode_key(self, key: object, padding_point: tuple[int, int]) -> tuple[int, int] | None: if not isinstance(key, (bytes, str)): return None try: @@ -92,16 +62,27 @@ def _decode_key(self, key: object) -> tuple[int, int] | None: except ValueError: return None if point.is_identity(): - return self.params.padding_point - return self.params.point_to_ring_point(point) + return padding_point + return int(point.x), int(point.y) def index_of(self, key: bytes | str) -> int: - point = self._decode_key(key) + padding_point = self.params.cv.curve.params.auxiliary_points.padding_point + point = self._decode_key(key, padding_point) if point is None: raise ValueError("invalid ring key") - if point == self.params.padding_point: + if point == padding_point: raise ValueError("producer key is not in ring") try: return self.nm_points[: self.params.max_ring_size].index(point) except ValueError as exc: raise ValueError("producer key is not in ring") from exc + + +def blinding_base_powers(params: RingProofParams, count: int) -> tuple[tuple[int, int], ...]: + point = params.cv.curve.params.auxiliary_points.blinding_base + points = [] + for _ in range(count): + points.append(point) + doubled = params.cv.point(point) + params.cv.point(point) + point = int(doubled.x), int(doubled.y) + return tuple(points) diff --git a/dot_ring/vrf/ring/proof_builder.py b/dot_ring/vrf/ring/proof_builder.py index 0fba6a0..4f32160 100644 --- a/dot_ring/vrf/ring/proof_builder.py +++ b/dot_ring/vrf/ring/proof_builder.py @@ -1,9 +1,9 @@ from __future__ import annotations from collections.abc import Sequence -from typing import cast from dot_ring.curve.curve import CurveVariant +from dot_ring.curve.point import CurvePoint from dot_ring.ring_proof.columns.columns import Column, WitnessColumnBuilder, require_commitment from dot_ring.ring_proof.constraints.constraints import RingConstraintBuilder from dot_ring.ring_proof.polynomial.fft import inverse_fft @@ -46,20 +46,21 @@ def build(self) -> RingProofPayload: params, ) witness_columns = witness_builder.build() - relation_point = witness_builder.result(params.blinding_base) - relation_plus_seed = witness_builder.result_p_seed(relation_point) + relation_point = witness_builder.result(params.cv.curve.params.auxiliary_points.blinding_base) + seed_point = params.cv.curve.params.auxiliary_points.accumulator_base + relation_plus_seed_point = relation_point + params.cv.point(seed_point) + relation_plus_seed = int(relation_plus_seed_point.x), int(relation_plus_seed_point.y) constraints = RingConstraintBuilder( - Result_plus_Seed=relation_plus_seed, - px=cast(list[int], ring_root.px.coeffs), - py=cast(list[int], ring_root.py.coeffs), - s=cast(list[int], ring_root.s.coeffs), - b=cast(list[int], witness_columns[0].coeffs), - acc_x=cast(list[int], witness_columns[1].coeffs), - acc_y=cast(list[int], witness_columns[2].coeffs), - acc_ip=cast(list[int], witness_columns[3].coeffs), + result_plus_seed=relation_plus_seed, + px=self._coeffs(ring_root.px), + py=self._coeffs(ring_root.py), + s=self._coeffs(ring_root.s), + b=self._coeffs(witness_columns[0]), + acc_x=self._coeffs(witness_columns[1]), + acc_y=self._coeffs(witness_columns[2]), + acc_ip=self._coeffs(witness_columns[3]), params=params, - seed_point=params.seed_point, ) transcript, alpha = self._phase1_transcript(ring_root, relation_point, witness_columns) constraint_polys = list(constraints.compute().values()) @@ -116,14 +117,14 @@ def build(self) -> RingProofPayload: accy_zeta=accy_zeta, c_q=c_q_column, l_zeta_omega=l_zeta_omega, - open_agg_zeta=phi_zeta, - open_l_zeta_omega=phi_zeta_omega, + open_agg_zeta=phi_zeta.proof, + open_l_zeta_omega=phi_zeta_omega.proof, ) def _phase1_transcript( self, ring_root: RingRoot, - relation_point: tuple[int, int], + relation_point: CurvePoint, witness_columns: tuple[Column, Column, Column, Column], ) -> tuple[FiatShamirTranscript, list[int]]: params = self.ring.params @@ -135,13 +136,10 @@ def _phase1_transcript( params.pcs.serialize_g1_uncompressed(require_commitment(c_accy)), ] transcript = ring_root.verifier_transcript_prefix(params, self.transcript_challenge).copy() - return cast( - tuple[FiatShamirTranscript, list[int]], - phase1_alphas_after_vk( - transcript, - relation_point, - witness_commitments, - ), + return phase1_alphas_after_vk( + transcript, + relation_point, + witness_commitments, ) def _aggregate_constraints( @@ -235,7 +233,7 @@ def _acc_x_constraint_factor(self, evals: dict[str, int], scalar_term: int) -> i b = evals["b_zeta"] x1, y1 = evals["acc_x_zeta"], evals["acc_y_zeta"] x2, y2 = evals["P_x_zeta"], evals["P_y_zeta"] - point_relation = y1 * y2 + params.ring_edwards_a * x1 * x2 + point_relation = y1 * y2 + params.cv.curve.params.a * x1 * x2 return (b * point_relation + (1 - b)) * scalar_term % params.prime def _acc_y_constraint_factor(self, evals: dict[str, int], scalar_term: int) -> int: diff --git a/dot_ring/vrf/ring/proof_payload.py b/dot_ring/vrf/ring/proof_payload.py index 93b4019..c7e5978 100644 --- a/dot_ring/vrf/ring/proof_payload.py +++ b/dot_ring/vrf/ring/proof_payload.py @@ -4,7 +4,6 @@ from dot_ring.ring_proof.columns.columns import Column, require_commitment from dot_ring.ring_proof.params import RingProofParams -from dot_ring.ring_proof.pcs.opening import Opening from dot_ring.ring_proof.pcs.protocol import G1Commitment RING_SCALAR_LEN = 32 @@ -14,6 +13,23 @@ def ring_proof_len(params: RingProofParams) -> int: return 7 * params.pcs.commitment_size + 8 * RING_SCALAR_LEN +def encode_scalar(value: int) -> bytes: + return value.to_bytes(RING_SCALAR_LEN, "little") + + +def decode_scalar(data: bytes, modulus: int) -> int: + if len(data) != RING_SCALAR_LEN: + raise ValueError(f"scalar must be exactly {RING_SCALAR_LEN} bytes, got {len(data)}") + value = int.from_bytes(data, "little") + if value >= modulus: + raise ValueError("scalar is not canonical") + return value + + +def _commitment_column(name: str, commitment: G1Commitment) -> Column: + return Column(name=name, evals=[], commitment=commitment) + + @dataclass(slots=True) class RingProofPayload: c_b: Column @@ -29,8 +45,8 @@ class RingProofPayload: accy_zeta: int c_q: Column l_zeta_omega: int - open_agg_zeta: Opening - open_l_zeta_omega: Opening + open_agg_zeta: G1Commitment + open_l_zeta_omega: G1Commitment def as_tuple( self, @@ -48,8 +64,8 @@ def as_tuple( int, Column, int, - Opening, - Opening, + G1Commitment, + G1Commitment, ]: return ( self.c_b, @@ -69,39 +85,43 @@ def as_tuple( self.open_l_zeta_omega, ) - def to_bytes(self, params: RingProofParams) -> bytes: + @staticmethod + def encoded_len(params: RingProofParams) -> int: + return ring_proof_len(params) + + def encode(self, params: RingProofParams) -> bytes: return b"".join( ( params.pcs.compress_g1(require_commitment(self.c_b)), params.pcs.compress_g1(require_commitment(self.c_accip)), params.pcs.compress_g1(require_commitment(self.c_accx)), params.pcs.compress_g1(require_commitment(self.c_accy)), - _scalar_to_bytes(self.px_zeta), - _scalar_to_bytes(self.py_zeta), - _scalar_to_bytes(self.s_zeta), - _scalar_to_bytes(self.b_zeta), - _scalar_to_bytes(self.accip_zeta), - _scalar_to_bytes(self.accx_zeta), - _scalar_to_bytes(self.accy_zeta), + encode_scalar(self.px_zeta), + encode_scalar(self.py_zeta), + encode_scalar(self.s_zeta), + encode_scalar(self.b_zeta), + encode_scalar(self.accip_zeta), + encode_scalar(self.accx_zeta), + encode_scalar(self.accy_zeta), params.pcs.compress_g1(require_commitment(self.c_q)), - _scalar_to_bytes(self.l_zeta_omega), - params.pcs.compress_g1(self.open_agg_zeta.proof), - params.pcs.compress_g1(self.open_l_zeta_omega.proof), + encode_scalar(self.l_zeta_omega), + params.pcs.compress_g1(self.open_agg_zeta), + params.pcs.compress_g1(self.open_l_zeta_omega), ) ) @classmethod - def from_bytes(cls, proof: bytes, params: RingProofParams) -> RingProofPayload: + def decode(cls, proof: bytes, params: RingProofParams) -> RingProofPayload: expected = ring_proof_len(params) if len(proof) != expected: raise ValueError(f"invalid Ring VRF proof length: expected {expected}, got {len(proof)}") reader = _PayloadReader(proof, params) - return cls( - c_b=Column(name="c_b", evals=[], commitment=reader.commitment()), - c_accip=Column(name="c_accip", evals=[], commitment=reader.commitment()), - c_accx=Column(name="c_accx", evals=[], commitment=reader.commitment()), - c_accy=Column(name="c_accy", evals=[], commitment=reader.commitment()), + payload = cls( + c_b=_commitment_column("c_b", reader.commitment()), + c_accip=_commitment_column("c_accip", reader.commitment()), + c_accx=_commitment_column("c_accx", reader.commitment()), + c_accy=_commitment_column("c_accy", reader.commitment()), px_zeta=reader.scalar(), py_zeta=reader.scalar(), s_zeta=reader.scalar(), @@ -109,11 +129,13 @@ def from_bytes(cls, proof: bytes, params: RingProofParams) -> RingProofPayload: accip_zeta=reader.scalar(), accx_zeta=reader.scalar(), accy_zeta=reader.scalar(), - c_q=Column(name="c_q", evals=[], commitment=reader.commitment()), + c_q=_commitment_column("c_q", reader.commitment()), l_zeta_omega=reader.scalar(), - open_agg_zeta=Opening(proof=reader.commitment(), y=0), - open_l_zeta_omega=Opening(proof=reader.commitment(), y=0), + open_agg_zeta=reader.commitment(), + open_l_zeta_omega=reader.commitment(), ) + reader.finish() + return payload class _PayloadReader: @@ -130,20 +152,11 @@ def commitment(self) -> G1Commitment: return commitment def scalar(self) -> int: - end = self.offset + 32 - value = _scalar_from_bytes(self.data[self.offset : end], self.params.prime) + end = self.offset + RING_SCALAR_LEN + value = decode_scalar(self.data[self.offset : end], self.params.prime) self.offset = end return value - -def _scalar_to_bytes(value: int) -> bytes: - return value.to_bytes(RING_SCALAR_LEN, "little") - - -def _scalar_from_bytes(data: bytes, modulus: int) -> int: - if len(data) != RING_SCALAR_LEN: - raise ValueError(f"scalar must be exactly {RING_SCALAR_LEN} bytes, got {len(data)}") - value = int.from_bytes(data, "little") - if value >= modulus: - raise ValueError("scalar is not canonical") - return value + def finish(self) -> None: + if self.offset != len(self.data): + raise ValueError(f"trailing bytes in ring proof payload: {len(self.data) - self.offset}") diff --git a/dot_ring/vrf/ring/root.py b/dot_ring/vrf/ring/root.py index 4d29691..26433ea 100644 --- a/dot_ring/vrf/ring/root.py +++ b/dot_ring/vrf/ring/root.py @@ -44,6 +44,10 @@ def _copy_commitment(commitment: Any) -> Any: return commitment.dup() if hasattr(commitment, "dup") else commitment +def _commitment_column(name: str, commitment: Any) -> Column: + return Column(name=name, evals=[], commitment=_copy_commitment(commitment)) + + def _transcript_g2_points(g2_points: Any) -> list[tuple[int, int]]: return [(b, a) for pair in g2_points for point in pair for a, b in [point]] @@ -103,7 +107,12 @@ def verifier_transcript_prefix(self, params: RingProofParams | None = None, tran transcript.absorb_labeled(b"vk", verifier_key_bytes) return transcript - def to_bytes(self) -> bytes: + @staticmethod + def encoded_len(params: RingProofParams | None = None) -> int: + commitment_size = params.pcs.commitment_size if params is not None else KZG.commitment_size + return 3 * commitment_size + + def encode(self) -> bytes: pcs = self.params.pcs if self.params is not None else KZG return b"".join( ( @@ -114,25 +123,22 @@ def to_bytes(self) -> bytes: ) @classmethod - def from_bytes(cls, data: bytes, params: RingProofParams | None = None) -> "RingRoot": - if params is not None: - commitment_size = params.pcs.commitment_size - else: - commitment_size = KZG.commitment_size - expected = 3 * commitment_size + def decode(cls, data: bytes, params: RingProofParams | None = None) -> "RingRoot": + commitment_size = params.pcs.commitment_size if params is not None else KZG.commitment_size + expected = cls.encoded_len(params) if len(data) != expected: raise ValueError(f"invalid ring root length: ring root must be exactly {expected} bytes, got {len(data)}") pcs = params.pcs if params is not None else KZG - px_commitment, py_commitment, s_commitment = ( - pcs.decompress_g1(data[0:commitment_size]), - pcs.decompress_g1(data[commitment_size : commitment_size * 2]), - pcs.decompress_g1(data[commitment_size * 2 : expected]), - ) + reader = _RingRootReader(data, commitment_size) + px_commitment = reader.g1(pcs) + py_commitment = reader.g1(pcs) + s_commitment = reader.g1(pcs) + reader.finish() - px = Column(name="px", evals=[], commitment=_copy_commitment(px_commitment)) - py = Column(name="py", evals=[], commitment=_copy_commitment(py_commitment)) - s = Column(name="s", evals=[], commitment=_copy_commitment(s_commitment)) + px = _commitment_column("px", px_commitment) + py = _commitment_column("py", py_commitment) + s = _commitment_column("s", s_commitment) return cls(px=px, py=py, s=s, params=params) @@ -145,4 +151,21 @@ def matches_ring(self, ring: Ring) -> bool: and tuple(self.py.evals[:domain_size]) == tuple(point[1] for point in ring.nm_points) and tuple(self.s.evals[:domain_size]) == tuple(1 if i < params.max_ring_size else 0 for i in range(domain_size)) ) - return RingRoot.from_ring(ring, params).to_bytes() == self.to_bytes() + return RingRoot.from_ring(ring, params).encode() == self.encode() + + +class _RingRootReader: + def __init__(self, data: bytes, commitment_size: int) -> None: + self.data = data + self.commitment_size = commitment_size + self.offset = 0 + + def g1(self, pcs: Any) -> Any: + end = self.offset + self.commitment_size + commitment = pcs.decompress_g1(self.data[self.offset : end]) + self.offset = end + return commitment + + def finish(self) -> None: + if self.offset != len(self.data): + raise ValueError(f"trailing bytes in ring root: {len(self.data) - self.offset}") diff --git a/dot_ring/vrf/ring/vrf.py b/dot_ring/vrf/ring/vrf.py index b84e8b1..410559d 100644 --- a/dot_ring/vrf/ring/vrf.py +++ b/dot_ring/vrf/ring/vrf.py @@ -1,23 +1,22 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, cast +from typing import Any from dot_ring.curve.point import CurvePoint from dot_ring.ring_proof.columns.columns import Column from dot_ring.ring_proof.params import RingProofParams -from dot_ring.ring_proof.pcs.opening import Opening +from dot_ring.ring_proof.pcs.protocol import G1Commitment from dot_ring.ring_proof.verify import RingProofFields, Verify from dot_ring.vrf.pedersen import PedersenVRF -from dot_ring.vrf.vrf import PreparedSecretKey from ..vrf import VRF -from .batch_verifier import RingBatchItem, RingBatchVerifier, _proof_relation_points, _proof_transcript_commitments +from .batch_verifier import RingBatchVerifier, _proof_relation_points, _proof_transcript_commitments from .context import RingContext, RingRootBuilder from .members import Ring from .members import parse_concatenated_keys as _parse_concatenated_keys from .proof_builder import RingProofBuilder -from .proof_payload import RingProofPayload, ring_proof_len +from .proof_payload import RingProofPayload from .root import RingRoot @@ -52,18 +51,18 @@ class RingVRF(VRF[Any]): accy_zeta: int c_q: Column l_zeta_omega: int - open_agg_zeta: Opening - open_l_zeta_omega: Opening + open_agg_zeta: G1Commitment + open_l_zeta_omega: G1Commitment @classmethod def proof_len(cls, skip_pedersen: bool = False) -> int: params = RingProofParams(cv=cls.cv) - proof_len = ring_proof_len(params) + proof_len = RingProofPayload.encoded_len(params) if skip_pedersen: return proof_len - return PedersenVRF[cast(Any, cls).cv].proof_len() + proof_len # type: ignore[misc] + return PedersenVRF[cls.cv].proof_len() + proof_len - def to_bytes(self) -> bytes: + def encode(self) -> bytes: """ Serialize the Ring VRF proof to bytes. @@ -71,10 +70,10 @@ def to_bytes(self) -> bytes: bytes: Bytes representation of the Ring VRF proof """ params = RingProofParams(cv=self.cv) - return cast(bytes, self.require_pedersen_proof().to_bytes()) + self._payload().to_bytes(params) + return self.require_pedersen_proof().encode() + self._payload().encode(params) @classmethod - def from_bytes(cls, proof: bytes, skip_pedersen: bool = False) -> RingVRF: + def decode(cls, proof: bytes, skip_pedersen: bool = False) -> RingVRF: """ Deserialize the Ring VRF proof from bytes. @@ -88,18 +87,18 @@ def from_bytes(cls, proof: bytes, skip_pedersen: bool = False) -> RingVRF: raise ValueError(f"invalid Ring VRF proof length: Ring VRF proof must be exactly {expected_len} bytes, got {len(proof)}") if not skip_pedersen: - pedersen_len = PedersenVRF[cast(Any, cls).cv].proof_len() # type: ignore[misc] - pedersen_proof = PedersenVRF[cast(Any, cls).cv].from_bytes(proof[:pedersen_len]) # type: ignore[misc] + pedersen_len = PedersenVRF[cls.cv].proof_len() + pedersen_proof = PedersenVRF[cls.cv].decode(proof[:pedersen_len]) offset = pedersen_len else: pedersen_proof = None offset = 0 params = RingProofParams(cv=cls.cv) - expected = offset + ring_proof_len(params) + expected = offset + RingProofPayload.encoded_len(params) if len(proof) != expected: raise ValueError(f"invalid Ring VRF proof length: expected {expected}, got {len(proof)}") - payload = RingProofPayload.from_bytes(proof[offset:], params) + payload = RingProofPayload.decode(proof[offset:], params) return cls( pedersen_proof=pedersen_proof, c_b=payload.c_b, @@ -154,8 +153,8 @@ def ring_proof_tuple(self) -> RingProofFields: self.accy_zeta, self.c_q.commitment, self.l_zeta_omega, - self.open_agg_zeta.proof, - self.open_l_zeta_omega.proof, + self.open_agg_zeta, + self.open_l_zeta_omega, ) def require_pedersen_proof(self) -> PedersenVRF: @@ -186,11 +185,11 @@ def _ring_proof_verifier( fixed_cols_cmts, rltn, res_plus_seed, - ring.params.seed_point, + ring.params.cv.point(ring.params.cv.curve.params.auxiliary_points.accumulator_base), ring.params.domain, transcript_prefix, padding_rows=ring.params.padding_rows, - edwards_a=ring.params.ring_edwards_a, + edwards_a=ring.params.cv.curve.params.a, prime=ring.params.prime, omega=ring.params.omega, pcs=ring.params.pcs, @@ -210,7 +209,7 @@ def verify_ring_proof( """ if not ring_root.matches_ring(ring): return False - return cast(bool, self._ring_proof_verifier(message, ring, ring_root).is_valid()) + return self._ring_proof_verifier(message, ring, ring_root).is_valid() @classmethod def prove( @@ -240,24 +239,10 @@ def prove( >>> ring = Ring(keys) # Automatically determines optimal domain size >>> proof = RingVRF[Bandersnatch].prove(alpha, ad, sk, pk, ring) """ - return cls.prove_prepared(alpha, ad, cls.prepare_secret_key(secret_key), producer_key, ring, ring_root) - - @classmethod - def prove_prepared( - cls, - alpha: bytes, - ad: bytes, - secret_key: PreparedSecretKey[Any], - producer_key: bytes, - ring: Ring, - ring_root: RingRoot | None = None, - ) -> RingVRF: - if secret_key.curve is not cls.cv: - raise ValueError("prepared secret key uses a different curve") - if producer_key != secret_key.public_key_bytes: + if producer_key != cls.cv.public_key_from_secret(secret_key): raise ValueError("producer_key does not match secret_key") - pedersen_proof = PedersenVRF[cast(Any, cls).cv].prove_prepared(alpha, secret_key, ad) # type: ignore[misc] + pedersen_proof = PedersenVRF[cls.cv].prove(alpha, secret_key, ad) ring_proof = RingProofBuilder( cls.cv, @@ -279,7 +264,7 @@ def parse_keys(cls, keys: bytes) -> list[bytes]: Returns: List[bytes]: A list of individual keys extracted from the input bytes object. """ - return cast(list[bytes], _parse_concatenated_keys(keys, cls.cv)) + return _parse_concatenated_keys(keys, cls.cv) def verify(self, input: bytes, ad_data: bytes, ring: Ring, ring_root: RingRoot) -> bool: """ @@ -297,7 +282,6 @@ def proof_to_hash(cls, gamma: CurvePoint, mul_cofactor: bool = False) -> bytes: __all__ = [ - "RingBatchItem", "RingBatchVerifier", "RingContext", "RingVRF", diff --git a/dot_ring/vrf/transcript.py b/dot_ring/vrf/transcript.py index c96ddd2..e3f2b2a 100644 --- a/dot_ring/vrf/transcript.py +++ b/dot_ring/vrf/transcript.py @@ -139,7 +139,7 @@ def vrf_transcript_scalars( t.absorb(bytes([scheme])) t.absorb(len(ios).to_bytes(8, "little")) for io in ios: - t.absorb(io.to_bytes()) + t.absorb(io.encode()) t.absorb(len(ad).to_bytes(8, "little")) t.absorb(ad) return t, DelinearizeScalars(curve, t) diff --git a/dot_ring/vrf/vrf.py b/dot_ring/vrf/vrf.py index 0fc7501..0d2acc1 100644 --- a/dot_ring/vrf/vrf.py +++ b/dot_ring/vrf/vrf.py @@ -1,24 +1,12 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import Any, ClassVar, Generic, Self, TypeVar, cast +from typing import Any, ClassVar, Generic, Self, TypeVar from ..curve.curve import CurveVariant -from ..curve.point import CurvePoint C = TypeVar("C", bound=CurveVariant) -@dataclass(frozen=True, slots=True) -class PreparedSecretKey(Generic[C]): - """Decoded VRF secret scalar with its matching public key.""" - - curve: CurveVariant - secret_scalar: int - public_key: CurvePoint - public_key_bytes: bytes - - class VRF(Generic[C]): """ Base VRF (Verifiable Random Function) implementation. @@ -48,23 +36,3 @@ def __class_getitem__(cls, curve_variant: CurveVariant | Any) -> type[Self] | An return cls new_class = type(f"{cls.__name__}[{curve_variant.name}]", (cls,), {"cv": curve_variant}) return new_class - - @classmethod - def get_public_key(cls, secret_key: bytes) -> bytes: - """Take the Secret_Key and return Public Key""" - return cls.prepare_secret_key(secret_key).public_key_bytes - - @classmethod - def prepare_secret_key(cls, secret_key: bytes) -> PreparedSecretKey[C]: - """Decode a secret key once and compute its matching public key.""" - from dot_ring.vrf.transcript import scalar_decode - - secret_key_int = scalar_decode(cls.cv, secret_key) - generator = cls.cv.generator_point() - public_key: CurvePoint = cast(Any, generator) * secret_key_int - return PreparedSecretKey( - curve=cls.cv, - secret_scalar=secret_key_int, - public_key=public_key, - public_key_bytes=public_key.point_to_string(), - ) diff --git a/scripts/generate_test_vectors.py b/scripts/generate_test_vectors.py index 8db4d2a..bbf4298 100644 --- a/scripts/generate_test_vectors.py +++ b/scripts/generate_test_vectors.py @@ -16,7 +16,6 @@ from dot_ring.curve.specs.ed25519 import Ed25519_TAI from dot_ring.curve.specs.jubjub import JubJub from dot_ring.curve.specs.p256 import P256_TAI -from dot_ring.keygen import secret_from_seed from dot_ring.ring_proof.params import RingProofParams from dot_ring.vrf.ietf import ThinVRF, TinyVRF from dot_ring.vrf.pedersen import PedersenVRF @@ -46,7 +45,6 @@ class Suite: RING_SUITES = [ Suite("bandersnatch_sha-512_ell2", Bandersnatch), - Suite("bandersnatch_sw_sha-512_tai", Bandersnatch_SW), Suite("bandersnatch_shake128_ell2", Bandersnatch_SHAKE128), Suite("jubjub_sha-512_tai", JubJub), ] @@ -73,7 +71,7 @@ def pedersen_len(curve: CurveVariant) -> int: def base_fields(suite: Suite, scheme: str, index: int, seed: int, alpha: bytes, ad: bytes) -> tuple[bytes, dict[str, str]]: - pk, sk = secret_from_seed(seed_bytes(seed), suite.curve) + pk, sk = suite.curve.secret_from_seed(seed_bytes(seed)) input_point = suite.curve.encode_to_curve(alpha) gamma = input_point * int.from_bytes(sk, "little") beta = TinyVRF[suite.curve].proof_to_hash(gamma) @@ -141,7 +139,7 @@ def ring_keys( if idx == prover_index: keys.append(prover_pk) else: - other_pk, _ = secret_from_seed(seed_bytes(0x11 + idx * 0x10 + seed), suite.curve) + other_pk, _ = suite.curve.secret_from_seed(seed_bytes(0x11 + idx * 0x10 + seed)) keys.append(other_pk) return keys @@ -158,7 +156,7 @@ def ring_vector(suite: Suite, index: int, seed: int, alpha: bytes, ad: bytes) -> if pedersen is None: raise RuntimeError("ring proof did not include a Pedersen proof") - proof_bytes = proof.to_bytes() + proof_bytes = proof.encode() vector.update( { "blinding": scalar_encode(suite.curve, pedersen._blinding_factor).hex(), @@ -168,7 +166,7 @@ def ring_vector(suite: Suite, index: int, seed: int, alpha: bytes, ad: bytes) -> "proof_s": scalar_encode(suite.curve, pedersen.s).hex(), "proof_sb": scalar_encode(suite.curve, pedersen.sb).hex(), "ring_pks": b"".join(keys).hex(), - "ring_pks_com": ring_root.to_bytes().hex(), + "ring_pks_com": ring_root.encode().hex(), "ring_proof": proof_bytes[pedersen_len(suite.curve) :].hex(), "ring_size": RING_SIZE, "prover_idx": PROVER_INDEX, diff --git a/tests/benchmark/bench_ietf.py b/tests/benchmark/bench_ietf.py index 2ce4e9e..facaf63 100644 --- a/tests/benchmark/bench_ietf.py +++ b/tests/benchmark/bench_ietf.py @@ -10,7 +10,6 @@ from dataclasses import dataclass from dot_ring import Bandersnatch -from dot_ring.keygen import secret_from_seed from dot_ring.vrf.ietf import TinyVRF @@ -39,7 +38,7 @@ def _seed(*parts: object) -> bytes: def benchmark_once(sample_index: int) -> Timing: - public_key, secret_key = secret_from_seed(_seed("ietf-signer", sample_index), Bandersnatch) + public_key, secret_key = Bandersnatch.secret_from_seed(_seed("ietf-signer", sample_index)) alpha = b"bench-ietf-input" + sample_index.to_bytes(8, "little") ad = b"bench-ietf-ad" + sample_index.to_bytes(8, "little") salt = b"bench-ietf-salt" + sample_index.to_bytes(8, "little") @@ -61,7 +60,7 @@ def benchmark_once(sample_index: int) -> Timing: prove_ms=prove_ms, verify_ms=verify_ms, total_ms=(time.perf_counter() - total_start) * 1000, - proof_size=len(proof.to_bytes()), + proof_size=len(proof.encode()), ) diff --git a/tests/benchmark/bench_pedersen.py b/tests/benchmark/bench_pedersen.py index cb7e733..5888172 100644 --- a/tests/benchmark/bench_pedersen.py +++ b/tests/benchmark/bench_pedersen.py @@ -10,7 +10,6 @@ from dataclasses import dataclass from dot_ring import Bandersnatch -from dot_ring.keygen import secret_from_seed from dot_ring.vrf.pedersen import PedersenVRF @@ -39,7 +38,7 @@ def _seed(*parts: object) -> bytes: def benchmark_once(sample_index: int) -> Timing: - _, secret_key = secret_from_seed(_seed("pedersen-signer", sample_index), Bandersnatch) + _, secret_key = Bandersnatch.secret_from_seed(_seed("pedersen-signer", sample_index)) alpha = b"bench-pedersen-input" + sample_index.to_bytes(8, "little") ad = b"bench-pedersen-ad" + sample_index.to_bytes(8, "little") salt = b"bench-pedersen-salt" + sample_index.to_bytes(8, "little") @@ -61,7 +60,7 @@ def benchmark_once(sample_index: int) -> Timing: prove_ms=prove_ms, verify_ms=verify_ms, total_ms=(time.perf_counter() - total_start) * 1000, - proof_size=len(proof.to_bytes()), + proof_size=len(proof.encode()), ) diff --git a/tests/benchmark/bench_ring_proof.py b/tests/benchmark/bench_ring_proof.py index 5a4b43c..5ec6f06 100644 --- a/tests/benchmark/bench_ring_proof.py +++ b/tests/benchmark/bench_ring_proof.py @@ -12,7 +12,6 @@ from dataclasses import dataclass from dot_ring import Bandersnatch -from dot_ring.keygen import secret_from_seed from dot_ring.ring_proof.params import RingProofParams from dot_ring.vrf.ring import RingBatchVerifier, RingRoot, RingVRF from dot_ring.vrf.ring.members import Ring @@ -62,7 +61,7 @@ def _seed(*parts: object) -> bytes: def _keypair(label: str, sample_index: int, member_index: int = 0) -> tuple[bytes, bytes]: - public_key, secret_key = secret_from_seed(_seed(label, sample_index, member_index), Bandersnatch) + public_key, secret_key = Bandersnatch.secret_from_seed(_seed(label, sample_index, member_index)) return public_key, secret_key @@ -104,12 +103,12 @@ def benchmark_once(ring_size: int, sample_index: int) -> Timing: raise AssertionError("ring proof verification failed") return Timing( - ring_id=ring_root.to_bytes(), + ring_id=ring_root.encode(), ring_root_ms=ring_root_ms, prove_ms=prove_ms, verify_ms=verify_ms, total_ms=(time.perf_counter() - total_start) * 1000, - proof_size=len(proof.to_bytes()), + proof_size=len(proof.encode()), ) @@ -144,7 +143,7 @@ def _prove_batch_fixture_range(ring_size: int, start: int, stop: int) -> list[Se alpha = b"bench-batch-input" + fixture_index.to_bytes(8, "little") ad = b"bench-batch-ad" + fixture_index.to_bytes(8, "little") proof = RingVRF[Bandersnatch].prove(alpha, ad, secret_key, public_key, ring, ring_root) - fixtures.append(SerializedProofFixture(fixture_index, proof.to_bytes(), alpha, ad)) + fixtures.append(SerializedProofFixture(fixture_index, proof.encode(), alpha, ad)) return fixtures @@ -179,7 +178,7 @@ def _build_batch_fixtures_parallel(ring_size: int, fixture_count: int, workers: serialized.sort(key=lambda fixture: fixture.index) fixtures = [ ProofFixture( - RingVRF[Bandersnatch].from_bytes(fixture.proof), + RingVRF[Bandersnatch].decode(fixture.proof), fixture.alpha, fixture.ad, ) @@ -236,19 +235,13 @@ def run( ring, ring_root, fixtures = _build_batch_fixtures_parallel(ring_size, fixture_count, workers) - start = time.perf_counter() - context = RingBatchVerifier.prepare_context(ring, ring_root) - prepared_fixtures = [context.prepare(fixture.proof, fixture.alpha, fixture.ad) for fixture in fixtures] - print() - print("batch_n verify_ms verify_ms_per_proof") + print("batch_n batch_ms batch_ms_per_proof") for batch_size in _batch_sizes(max_batch_size): - items = prepared_fixtures[:batch_size] - verifier = RingBatchVerifier(context) - for item in items: - verifier.push_prepared(item) - + verifier = RingBatchVerifier() start = time.perf_counter() + for fixture in fixtures[:batch_size]: + verifier.push(fixture.proof, fixture.alpha, fixture.ad, ring, ring_root) verified = verifier.verify() elapsed_ms = (time.perf_counter() - start) * 1000 if not verified: @@ -262,7 +255,7 @@ def main() -> None: parser.add_argument("n", type=int, nargs="?", default=1024, help="ring size") parser.add_argument("-s", "--samples", type=int, default=3, help="number of fresh samples to run") parser.add_argument("--batch-max", type=int, default=256, help="largest batched verification size to benchmark") - parser.add_argument("--batch-fixtures", type=int, default=None, help="number of unique proofs to prepare; defaults to --batch-max") + parser.add_argument("--batch-fixtures", type=int, default=None, help="number of unique proofs to generate; defaults to --batch-max") parser.add_argument("--batch-workers", type=int, default=os.cpu_count() or 1, help="parallel worker processes for batch proof generation") args = parser.parse_args() run(args.n, args.samples, args.batch_max, args.batch_fixtures, args.batch_workers) diff --git a/tests/benchmark/memory_regression.py b/tests/benchmark/memory_regression.py index b6bcf88..92495ba 100644 --- a/tests/benchmark/memory_regression.py +++ b/tests/benchmark/memory_regression.py @@ -13,7 +13,6 @@ from pathlib import Path from dot_ring import Bandersnatch -from dot_ring.keygen import secret_from_seed from dot_ring.ring_proof.params import RingProofParams from dot_ring.ring_proof.pcs.kzg import KZG from dot_ring.vrf.ring import RingRoot, RingVRF @@ -50,14 +49,14 @@ def _seed(*parts: object) -> bytes: def _ring_fixture(ring_size: int, sample_index: int) -> tuple[bytes, bytes, bytes, bytes, Ring, RingRoot]: - public_key, secret_key = secret_from_seed(_seed("memory-signer", sample_index), Bandersnatch) + public_key, secret_key = Bandersnatch.secret_from_seed(_seed("memory-signer", sample_index)) signer_index = min(SIGNER_INDEX, ring_size - 1) keys: list[bytes] = [] for member_index in range(ring_size): if member_index == signer_index: keys.append(public_key) else: - member_key, _ = secret_from_seed(_seed("memory-ring-member", sample_index, member_index), Bandersnatch) + member_key, _ = Bandersnatch.secret_from_seed(_seed("memory-ring-member", sample_index, member_index)) keys.append(member_key) params = RingProofParams.from_ring_size(ring_size) diff --git a/tests/test_ark_vrf.py b/tests/test_ark_vrf.py index 5cb2bb5..077e924 100644 --- a/tests/test_ark_vrf.py +++ b/tests/test_ark_vrf.py @@ -49,14 +49,14 @@ def test_tiny_vectors(curve, prefix: str, point_len: int) -> None: alpha = bytes.fromhex(vector["alpha"]) ad = bytes.fromhex(vector["ad"]) proof = TinyVRF[curve].prove(alpha, sk, ad) - proof_bytes = proof.to_bytes() + proof_bytes = proof.encode() - assert TinyVRF[curve].get_public_key(sk).hex() == vector["pk"] + assert curve.public_key_from_secret(sk).hex() == vector["pk"] assert curve.encode_to_curve(alpha).point_to_string().hex() == vector["h"] assert proof_bytes == tiny_proof_bytes(vector) assert TinyVRF[curve].proof_to_hash(proof.output_point).hex() == vector["beta"] assert proof.verify(bytes.fromhex(vector["pk"]), alpha, ad) - assert TinyVRF[curve].from_bytes(proof_bytes).to_bytes() == proof_bytes + assert TinyVRF[curve].decode(proof_bytes).encode() == proof_bytes assert len(proof.output_point.point_to_string()) == point_len @@ -67,11 +67,11 @@ def test_thin_vectors(curve, prefix: str, _point_len: int) -> None: alpha = bytes.fromhex(vector["alpha"]) ad = bytes.fromhex(vector["ad"]) proof = ThinVRF[curve].prove(alpha, sk, ad) - proof_bytes = proof.to_bytes() + proof_bytes = proof.encode() assert proof_bytes == thin_proof_bytes(vector) assert proof.verify(bytes.fromhex(vector["pk"]), alpha, ad) - assert ThinVRF[curve].from_bytes(proof_bytes).to_bytes() == proof_bytes + assert ThinVRF[curve].decode(proof_bytes).encode() == proof_bytes @pytest.mark.parametrize("curve,prefix,_point_len", SUITES) @@ -81,11 +81,11 @@ def test_pedersen_vectors(curve, prefix: str, _point_len: int) -> None: alpha = bytes.fromhex(vector["alpha"]) ad = bytes.fromhex(vector["ad"]) proof = PedersenVRF[curve].prove(alpha, sk, ad) - proof_bytes = proof.to_bytes() + proof_bytes = proof.encode() assert proof_bytes == pedersen_proof_bytes(vector) assert proof.verify(alpha, ad) - assert PedersenVRF[curve].from_bytes(proof_bytes).to_bytes() == proof_bytes + assert PedersenVRF[curve].decode(proof_bytes).encode() == proof_bytes @pytest.mark.parametrize("curve,prefix,point_len", SUITES) @@ -95,24 +95,23 @@ def test_rejects_invalid_point_encodings(curve, prefix: str, point_len: int) -> tiny_vector = load(f"{prefix}_tiny.json")[0] tiny_bytes = invalid_point + bytes_from_fields(tiny_vector, "proof_c", "proof_s") with pytest.raises(ValueError, match="INVALID|Invalid"): - TinyVRF[curve].from_bytes(tiny_bytes) + TinyVRF[curve].decode(tiny_bytes) thin_vector = load(f"{prefix}_thin.json")[0] thin_bytes = invalid_point + bytes_from_fields(thin_vector, "proof_r", "proof_s") with pytest.raises(ValueError, match="INVALID|Invalid"): - ThinVRF[curve].from_bytes(thin_bytes) + ThinVRF[curve].decode(thin_bytes) pedersen_vector = load(f"{prefix}_pedersen.json")[0] pedersen_bytes = invalid_point + bytes_from_fields(pedersen_vector, "proof_pk_com", "proof_r", "proof_ok", "proof_s", "proof_sb") with pytest.raises(ValueError, match="INVALID|Invalid"): - PedersenVRF[curve].from_bytes(pedersen_bytes) + PedersenVRF[curve].decode(pedersen_bytes) @pytest.mark.parametrize( "curve,filename", [ (Bandersnatch, "bandersnatch_sha-512_ell2_ring.json"), - (Bandersnatch_SW, "bandersnatch_sw_sha-512_tai_ring.json"), (Bandersnatch_SHAKE128, "bandersnatch_shake128_ell2_ring.json"), (JubJub, "jubjub_sha-512_tai_ring.json"), ], @@ -126,17 +125,17 @@ def test_ring_vectors(curve, filename: str) -> None: params = RingProofParams(test_vectors=True, cv=curve) ring = Ring(keys, params) ring_root = RingRoot.from_ring(ring, params) - proof = RingVRF[curve].from_bytes(proof_bytes) + proof = RingVRF[curve].decode(proof_bytes) - assert ring_root.to_bytes().hex() == vector["ring_pks_com"] + assert ring_root.encode().hex() == vector["ring_pks_com"] assert proof.verify(alpha, ad, ring, ring_root) generated = RingVRF[curve].prove(alpha, ad, bytes.fromhex(vector["sk"]), bytes.fromhex(vector["pk"]), ring, ring_root) - assert generated.to_bytes() == proof_bytes + assert generated.encode() == proof_bytes def test_multi_input_apis() -> None: secret = bytes.fromhex(load("bandersnatch_sha-512_ell2_tiny.json")[0]["sk"]) - public_key = TinyVRF[Bandersnatch].get_public_key(secret) + public_key = Bandersnatch.public_key_from_secret(secret) secret_scalar = int.from_bytes(secret, "little") public_key_point = Bandersnatch.string_to_point(public_key) @@ -210,7 +209,7 @@ def test_ring_batch_same_ring_and_invalid_item() -> None: params = RingProofParams(test_vectors=True) ring = Ring(keys, params) ring_root = RingRoot.from_ring(ring, params) - proof = RingVRF[Bandersnatch].from_bytes(ring_proof_bytes(vector)) + proof = RingVRF[Bandersnatch].decode(ring_proof_bytes(vector)) second_alpha = b"ring-batch-second" second_ad = b"ring-batch-ad" second_proof = RingVRF[Bandersnatch].prove( @@ -244,7 +243,7 @@ def test_ring_batch_multi_ring_shared_srs(monkeypatch: pytest.MonkeyPatch) -> No params = RingProofParams(test_vectors=True) ring = Ring(keys, params) ring_root = RingRoot.from_ring(ring, params) - proof = RingVRF[Bandersnatch].from_bytes(ring_proof_bytes(vector)) + proof = RingVRF[Bandersnatch].decode(ring_proof_bytes(vector)) batch.push(proof, bytes.fromhex(vector["alpha"]), bytes.fromhex(vector["ad"]), ring, ring_root) def fail_single_verify(*_args: object, **_kwargs: object) -> bool: @@ -264,10 +263,10 @@ def test_ring_context_helpers() -> None: builder = context.ring_root_builder() builder.extend(keys_blob) - assert ring_root.to_bytes().hex() == vector["ring_pks_com"] - assert context.ring_root(keys_blob).to_bytes() == ring_root.to_bytes() - assert context.ring_root_from_bytes(ring_root.to_bytes()).to_bytes() == ring_root.to_bytes() - assert builder.finalize().to_bytes() == ring_root.to_bytes() + assert ring_root.encode().hex() == vector["ring_pks_com"] + assert context.ring_root(keys_blob).encode() == ring_root.encode() + assert context.decode_ring_root(ring_root.encode()).encode() == ring_root.encode() + assert builder.finalize().encode() == ring_root.encode() def test_negative_and_malformed_proofs() -> None: @@ -285,16 +284,16 @@ def test_negative_and_malformed_proofs() -> None: assert not tiny.verify(pk, b"wrong-input", ad) assert not tiny.verify_ios(pk_point, [VrfIo(Bandersnatch.encode_to_curve(alpha), wrong_output)], ad) with pytest.raises(ValueError, match="invalid Tiny VRF proof length"): - TinyVRF[Bandersnatch].from_bytes(tiny.to_bytes()[:-1]) + TinyVRF[Bandersnatch].decode(tiny.encode()[:-1]) thin = ThinVRF[Bandersnatch].prove(alpha, sk, ad) with pytest.raises(ValueError, match="invalid Thin VRF proof length"): - ThinVRF[Bandersnatch].from_bytes(thin.to_bytes()[:-1]) + ThinVRF[Bandersnatch].decode(thin.encode()[:-1]) pedersen = PedersenVRF[Bandersnatch].prove(alpha, sk, ad) assert not pedersen.verify(alpha, b"wrong-ad") with pytest.raises(ValueError, match="invalid Pedersen VRF proof length"): - PedersenVRF[Bandersnatch].from_bytes(pedersen.to_bytes()[:-1]) + PedersenVRF[Bandersnatch].decode(pedersen.encode()[:-1]) ring_vector = load("bandersnatch_sha-512_ell2_ring.json")[0] ring_alpha = bytes.fromhex(ring_vector["alpha"]) @@ -305,23 +304,23 @@ def test_negative_and_malformed_proofs() -> None: ring = Ring(keys, params) ring_root = RingRoot.from_ring(ring, params) wrong_root = RingRoot.from_ring(Ring(list(reversed(keys)), params), params) - proof = RingVRF[Bandersnatch].from_bytes(proof_bytes) + proof = RingVRF[Bandersnatch].decode(proof_bytes) assert not proof.verify(ring_alpha, b"wrong-ad", ring, ring_root) assert not proof.verify(b"wrong-input", ring_ad, ring, ring_root) assert not proof.verify(ring_alpha, ring_ad, ring, wrong_root) with pytest.raises(ValueError, match="invalid Ring VRF proof length"): - RingVRF[Bandersnatch].from_bytes(proof_bytes[:-1]) + RingVRF[Bandersnatch].decode(proof_bytes[:-1]) pedersen_len = len(proof_bytes) - len(bytes.fromhex(ring_vector["ring_proof"])) invalid_kzg_point = proof_bytes[:pedersen_len] + b"\xff" * 48 + proof_bytes[pedersen_len + 48 :] with pytest.raises(ValueError): - RingVRF[Bandersnatch].from_bytes(invalid_kzg_point) + RingVRF[Bandersnatch].decode(invalid_kzg_point) with pytest.raises(ValueError, match="invalid Ring VRF proof length"): - RingVRF[Bandersnatch].from_bytes(bytes.fromhex(ring_vector["ring_proof"])[:-1], skip_pedersen=True) + RingVRF[Bandersnatch].decode(bytes.fromhex(ring_vector["ring_proof"])[:-1], skip_pedersen=True) with pytest.raises(ValueError, match="invalid ring root length"): - RingRoot.from_bytes(ring_root.to_bytes()[:-1]) + RingRoot.decode(ring_root.encode()[:-1]) with pytest.raises(ValueError): - RingRoot.from_bytes(b"\xff" * 48 + ring_root.to_bytes()[48:]) + RingRoot.decode(b"\xff" * 48 + ring_root.encode()[48:]) def test_ring_rejects_wrong_prover_key() -> None: @@ -333,7 +332,7 @@ def test_ring_rejects_wrong_prover_key() -> None: ring = Ring(keys, params) ring_root = RingRoot.from_ring(ring, params) wrong_secret = bytes.fromhex(load("bandersnatch_sha-512_ell2_tiny.json")[1]["sk"]) - wrong_pk = TinyVRF[Bandersnatch].get_public_key(wrong_secret) + wrong_pk = Bandersnatch.public_key_from_secret(wrong_secret) with pytest.raises(ValueError, match="producer key is not in ring"): RingVRF[Bandersnatch].prove(alpha, ad, wrong_secret, wrong_pk, ring, ring_root) diff --git a/tests/test_bandersnatch_ark.py b/tests/test_bandersnatch_ark.py index f209f09..d59d4bc 100644 --- a/tests/test_bandersnatch_ark.py +++ b/tests/test_bandersnatch_ark.py @@ -30,7 +30,7 @@ def test_ietf_ark_bandersnatch(): ) # Public Key check - pk_bytes = TinyVRF[Bandersnatch].get_public_key(secret_scalar) + pk_bytes = Bandersnatch.public_key_from_secret(secret_scalar) public_key = Bandersnatch.string_to_point(pk_bytes) assert public_key.point_to_string().hex() == vector["pk"] @@ -40,8 +40,8 @@ def test_ietf_ark_bandersnatch(): assert input_point.point_to_string().hex() == vector["h"] proof = TinyVRF[Bandersnatch].prove(alpha, secret_scalar, additional_data) - proof_bytes = proof.to_bytes() - proof_rt = TinyVRF[Bandersnatch].from_bytes(proof_bytes) + proof_bytes = proof.encode() + proof_rt = TinyVRF[Bandersnatch].decode(proof_bytes) # Proof components check gamma = proof_bytes[:gamma_len] @@ -56,7 +56,7 @@ def test_ietf_ark_bandersnatch(): assert TinyVRF[Bandersnatch].ecvrf_proof_to_hash(proof_bytes).hex() == vector["beta"] assert proof.verify(pk_bytes, alpha, additional_data) - assert proof_rt.to_bytes() == proof_bytes + assert proof_rt.encode() == proof_bytes assert proof_rt.verify(pk_bytes, alpha, additional_data) @@ -74,7 +74,7 @@ def test_pedersen_ark_bandersnatch(): ) # Public Key check - pk_bytes = PedersenVRF[Bandersnatch].get_public_key(secret_scalar) + pk_bytes = Bandersnatch.public_key_from_secret(secret_scalar) # Input Point check input_point = Bandersnatch.encode_to_curve(alpha) @@ -82,8 +82,8 @@ def test_pedersen_ark_bandersnatch(): assert input_point.point_to_string().hex() == vector["h"] proof = PedersenVRF[Bandersnatch].prove(alpha, secret_scalar, additional_data) - proof_bytes = proof.to_bytes() - proof_rt = PedersenVRF[Bandersnatch].from_bytes(proof_bytes) + proof_bytes = proof.encode() + proof_rt = PedersenVRF[Bandersnatch].decode(proof_bytes) assert pk_bytes.hex() == vector["pk"], "Invalid Public Key" assert proof.output_point.point_to_string().hex() == vector["gamma"] @@ -104,7 +104,7 @@ def test_pedersen_ark_bandersnatch(): assert PedersenVRF[Bandersnatch].ecvrf_proof_to_hash(proof.output_point.point_to_string()).hex() == vector["beta"] assert proof.verify(alpha, additional_data) - assert proof_rt.to_bytes() == proof_bytes + assert proof_rt.encode() == proof_bytes assert proof_rt.verify(alpha, additional_data) @@ -126,22 +126,22 @@ def test_ring_proof(): ring_root = RingRoot.from_ring(ring, params) ring_time = time() print(f"\nTime taken for Ring Root Construction: \t\t {1000 * (ring_time - start):.2f} ms") - p_k = RingVRF[Bandersnatch].get_public_key(s_k) + p_k = Bandersnatch.public_key_from_secret(s_k) ring_vrf_proof = RingVRF[Bandersnatch].prove(alpha, ad, s_k, p_k, ring, ring_root) pk_time = time() print(f"Time taken for Proof Generation: \t {1000 * (pk_time - ring_time):.2f} ms") - proof_bytes = ring_vrf_proof.to_bytes() - proof_rt = RingVRF[Bandersnatch].from_bytes(proof_bytes) + proof_bytes = ring_vrf_proof.encode() + proof_rt = RingVRF[Bandersnatch].decode(proof_bytes) assert p_k.hex() == item["pk"], "Invalid Public Key" - assert ring_root.to_bytes().hex() == item["ring_pks_com"], "Invalid Ring Root" + assert ring_root.encode().hex() == item["ring_pks_com"], "Invalid Ring Root" assert ( - ring_vrf_proof.to_bytes().hex() + ring_vrf_proof.encode().hex() == item["gamma"] + item["proof_pk_com"] + item["proof_r"] + item["proof_ok"] + item["proof_s"] + item["proof_sb"] + item["ring_proof"] ), "Unexpected Proof" assert ring_vrf_proof.verify(alpha, ad, ring, ring_root), "Verification Failed" - assert proof_rt.to_bytes() == proof_bytes + assert proof_rt.encode() == proof_bytes start = time() assert proof_rt.verify(alpha, ad, ring, ring_root) verify_time = time() diff --git a/tests/test_coverage/test_columns.py b/tests/test_coverage/test_columns.py index eedf2b6..ed8eb31 100644 --- a/tests/test_coverage/test_columns.py +++ b/tests/test_coverage/test_columns.py @@ -4,7 +4,7 @@ from dot_ring.ring_proof.columns.columns import Column, WitnessColumnBuilder from dot_ring.ring_proof.constants import DEFAULT_SIZE, OMEGAS, S_PRIME from dot_ring.ring_proof.params import RingProofParams -from dot_ring.vrf.ring import Ring, RingVRF +from dot_ring.vrf.ring import Ring def test_column_interpolate_rejects_oversize_evals(): @@ -33,7 +33,7 @@ def test_ring_from_params(): def test_ring_rejects_oversize_ring(): """Test Ring rejects rings that are too large""" params = RingProofParams(domain_size=512, max_ring_size=2, padding_rows=4) - keys = [RingVRF[Bandersnatch].get_public_key(i.to_bytes(32, "little")) for i in range(1, 4)] + keys = [Bandersnatch.public_key_from_secret(i.to_bytes(32, "little")) for i in range(1, 4)] with pytest.raises(ValueError, match="exceeds max supported size"): Ring(keys, params) diff --git a/tests/test_coverage/test_params.py b/tests/test_coverage/test_params.py index a4cdeb1..22f3ff1 100644 --- a/tests/test_coverage/test_params.py +++ b/tests/test_coverage/test_params.py @@ -76,7 +76,7 @@ def test_ring_proof_params_defaults(): def test_baby_jubjub_ring_proof_params_are_unsupported(): - with pytest.raises(ValueError, match="BabyJubJub ring proofs are not supported"): + with pytest.raises(ValueError, match="BabyJubJub ring proofs require field modulus"): RingProofParams(cv=BabyJubJub) diff --git a/tests/test_curve_ops/test_gaps.py b/tests/test_curve_ops/test_gaps.py index 8bf0340..636d546 100644 --- a/tests/test_curve_ops/test_gaps.py +++ b/tests/test_curve_ops/test_gaps.py @@ -15,7 +15,7 @@ def test_ecvrf_decode_proof_string_input(self): sk = b"secret" ad = b"ad" proof = TinyVRF[Bandersnatch].prove(alpha, sk, ad) - proof_bytes = proof.to_bytes() + proof_bytes = proof.encode() proof_hex = proof_bytes.hex() gamma, c, s = TinyVRF[Bandersnatch].ecvrf_decode_proof(proof_hex) @@ -42,7 +42,7 @@ def test_ecvrf_decode_proof_invalid_s(self): sk = b"secret" ad = b"ad" proof = TinyVRF[Bandersnatch].prove(alpha, sk, ad) - proof_bytes = proof.to_bytes() + proof_bytes = proof.encode() # Modify S to be >= order order = Bandersnatch.curve.params.subgroup_order @@ -68,14 +68,14 @@ def test_ecvrf_proof_to_hash_string_input(self): sk = b"secret" ad = b"ad" proof = TinyVRF[Bandersnatch].prove(alpha, sk, ad) - proof_bytes = proof.to_bytes() + proof_bytes = proof.encode() proof_hex = proof_bytes.hex() hash_bytes = TinyVRF[Bandersnatch].ecvrf_proof_to_hash(proof_hex) assert isinstance(hash_bytes, bytes) - def test_ietf_from_bytes_invalid_point(self): - """Test TinyVRF.from_bytes raises ValueError for invalid output point.""" + def test_ietf_decode_invalid_point(self): + """Test TinyVRF.decode raises ValueError for invalid output point.""" invalid_point = b"\xff" * point_len(Bandersnatch) c_len = Bandersnatch.curve.params.encoding.challenge_len s_len = scalar_len(Bandersnatch) @@ -85,15 +85,15 @@ def test_ietf_from_bytes_invalid_point(self): invalid_proof = invalid_point + dummy_c + dummy_s with pytest.raises(ValueError, match="Invalid output point"): - TinyVRF[Bandersnatch].from_bytes(invalid_proof) + TinyVRF[Bandersnatch].decode(invalid_proof) - def test_ietf_from_bytes_invalid_s(self): - """Test TinyVRF.from_bytes raises ValueError if s >= order.""" + def test_ietf_decode_invalid_s(self): + """Test TinyVRF.decode raises ValueError if s >= order.""" alpha = b"test" sk = b"secret" ad = b"ad" proof = TinyVRF[Bandersnatch].prove(alpha, sk, ad) - proof_bytes = proof.to_bytes() + proof_bytes = proof.encode() order = Bandersnatch.curve.params.subgroup_order invalid_s = order + 1 @@ -110,7 +110,7 @@ def test_ietf_from_bytes_invalid_s(self): invalid_proof = gamma_bytes + c_bytes + invalid_s_bytes with pytest.raises(ValueError, match="Response scalar s is not less than the curve order"): - TinyVRF[Bandersnatch].from_bytes(invalid_proof) + TinyVRF[Bandersnatch].decode(invalid_proof) def test_ietf_verify_invalid_public_key(self): """Test TinyVRF.verify raises ValueError for invalid public key.""" @@ -124,14 +124,14 @@ def test_ietf_verify_invalid_public_key(self): with pytest.raises(ValueError, match="Invalid public key"): proof.verify(invalid_pk, alpha, ad) - def test_pedersen_from_bytes_invalid_point(self): - """Test PedersenVRF.from_bytes raises ValueError for invalid points.""" + def test_pedersen_decode_invalid_point(self): + """Test PedersenVRF.decode raises ValueError for invalid points.""" point_len = 33 scalar_len = 32 invalid_proof = b"\xff" * (point_len * 4 + scalar_len * 2) with pytest.raises(ValueError): # Message might vary depending on which point fails first - PedersenVRF[Bandersnatch].from_bytes(invalid_proof) + PedersenVRF[Bandersnatch].decode(invalid_proof) def test_pedersen_proof_to_hash_string(self): """Test PedersenVRF.ecvrf_proof_to_hash handles hex string input.""" @@ -160,7 +160,7 @@ def test_uncompressed_curve_decode(self): try: proof = TinyVRF[Ed448_RO].prove(alpha, sk, ad) - proof_bytes = proof.to_bytes() + proof_bytes = proof.encode() # Decode gamma, c, s = TinyVRF[Ed448_RO].ecvrf_decode_proof(proof_bytes) @@ -168,26 +168,26 @@ def test_uncompressed_curve_decode(self): except Exception as e: pytest.skip(f"Ed448 not fully supported or failed: {e}") - def test_ring_from_bytes_skip_pedersen(self): - """Test RingVRF.from_bytes with skip_pedersen=True.""" + def test_ring_decode_skip_pedersen(self): + """Test RingVRF.decode with skip_pedersen=True.""" from dot_ring.vrf.ring import RingVRF # Generate a valid proof alpha = b"test" ad = b"ad" sk = b"secret" - pk = PedersenVRF[Bandersnatch].get_public_key(sk) + pk = Bandersnatch.public_key_from_secret(sk) keys = [pk] params = RingProofParams() ring = Ring(keys, params) ring_root = RingRoot.from_ring(ring, params) proof = RingVRF[Bandersnatch].prove(alpha, ad, sk, pk, ring, ring_root) - proof_bytes = proof.to_bytes() + proof_bytes = proof.encode() # skip_pedersen=True expects bytes that start at the ring proof payload. ring_proof_bytes = proof_bytes[192:] - parsed = RingVRF[Bandersnatch].from_bytes(ring_proof_bytes, skip_pedersen=True) + parsed = RingVRF[Bandersnatch].decode(ring_proof_bytes, skip_pedersen=True) assert parsed.pedersen_proof is None def test_ring_verify_missing_pedersen(self): @@ -199,21 +199,21 @@ def test_ring_verify_missing_pedersen(self): alpha = b"test" ad = b"ad" sk = b"secret" - pk = PedersenVRF[Bandersnatch].get_public_key(sk) + pk = Bandersnatch.public_key_from_secret(sk) keys = [pk] params = RingProofParams() ring = Ring(keys, params) ring_root = RingRoot.from_ring(ring, params) proof = RingVRF[Bandersnatch].prove(alpha, ad, sk, pk, ring, ring_root) - proof_bytes = proof.to_bytes() + proof_bytes = proof.encode() ring_proof_bytes = proof_bytes[192:] - parsed = RingVRF[Bandersnatch].from_bytes(ring_proof_bytes, skip_pedersen=True) + parsed = RingVRF[Bandersnatch].decode(ring_proof_bytes, skip_pedersen=True) with pytest.raises(ValueError, match="Pedersen proof is missing"): parsed.verify(alpha, ad, ring, ring_root) - def test_ring_construct_ring_root_invalid_keys(self): + def test_ring_handles_invalid_keys(self): """Test Ring construction with invalid keys.""" # Invalid key string invalid_key = b"invalid" @@ -238,7 +238,7 @@ def test_ring_verify_ring_proof_bytes_input(self): alpha = b"test" ad = b"ad" sk = b"secret" - pk = PedersenVRF[Bandersnatch].get_public_key(sk) + pk = Bandersnatch.public_key_from_secret(sk) keys = [pk] params = RingProofParams() @@ -258,7 +258,7 @@ def test_ring_verify_ring_proof_invalid_message(self): alpha = b"test" ad = b"ad" sk = b"secret" - pk = PedersenVRF[Bandersnatch].get_public_key(sk) + pk = Bandersnatch.public_key_from_secret(sk) keys = [pk] params = RingProofParams() diff --git a/tests/test_dot_ring_vectors.py b/tests/test_dot_ring_vectors.py index f8e1ad4..b4826c6 100644 --- a/tests/test_dot_ring_vectors.py +++ b/tests/test_dot_ring_vectors.py @@ -23,9 +23,9 @@ def test_dot_ring_tiny_vectors(suite: Suite) -> None: alpha = bytes.fromhex(vector["alpha"]) ad = bytes.fromhex(vector["ad"]) proof_bytes = tiny_proof_bytes(vector) - proof = TinyVRF[suite.curve].from_bytes(proof_bytes) + proof = TinyVRF[suite.curve].decode(proof_bytes) - assert TinyVRF[suite.curve].get_public_key(bytes.fromhex(vector["sk"])).hex() == vector["pk"] + assert suite.curve.public_key_from_secret(bytes.fromhex(vector["sk"])).hex() == vector["pk"] assert suite.curve.encode_to_curve(alpha).point_to_string().hex() == vector["h"] assert proof.verify(bytes.fromhex(vector["pk"]), alpha, ad) assert TinyVRF[suite.curve].proof_to_hash(proof.output_point).hex() == vector["beta"] @@ -37,7 +37,7 @@ def test_dot_ring_thin_vectors(suite: Suite) -> None: alpha = bytes.fromhex(vector["alpha"]) ad = bytes.fromhex(vector["ad"]) proof_bytes = thin_proof_bytes(vector) - proof = ThinVRF[suite.curve].from_bytes(proof_bytes) + proof = ThinVRF[suite.curve].decode(proof_bytes) assert proof.verify(bytes.fromhex(vector["pk"]), alpha, ad) assert ThinVRF[suite.curve].proof_to_hash(proof.output_point).hex() == vector["beta"] @@ -49,7 +49,7 @@ def test_dot_ring_pedersen_vectors(suite: Suite) -> None: alpha = bytes.fromhex(vector["alpha"]) ad = bytes.fromhex(vector["ad"]) proof_bytes = pedersen_proof_bytes(vector) - proof = PedersenVRF[suite.curve].from_bytes(proof_bytes) + proof = PedersenVRF[suite.curve].decode(proof_bytes) assert proof.verify(alpha, ad) assert PedersenVRF[suite.curve].proof_to_hash(proof.output_point).hex() == vector["beta"] @@ -64,11 +64,11 @@ def test_dot_ring_ring_vectors(suite: Suite) -> None: params = RingProofParams(test_vectors=True, cv=suite.curve) ring = Ring(keys, params) ring_root = RingRoot.from_ring(ring, params) - proof = RingVRF[suite.curve].from_bytes(ring_proof_bytes(vector)) + proof = RingVRF[suite.curve].decode(ring_proof_bytes(vector)) assert proof.pedersen_proof is not None assert len(keys) == vector["ring_size"] assert keys[vector["prover_idx"]] == bytes.fromhex(vector["pk"]) - assert ring_root.to_bytes().hex() == vector["ring_pks_com"] + assert ring_root.encode().hex() == vector["ring_pks_com"] assert proof.verify(alpha, ad, ring, ring_root) assert RingVRF[suite.curve].proof_to_hash(proof.pedersen_proof.output_point).hex() == vector["beta"] diff --git a/tests/test_ietf/test_ietf_ark.py b/tests/test_ietf/test_ietf_ark.py index 7f3229c..95e5d1e 100644 --- a/tests/test_ietf/test_ietf_ark.py +++ b/tests/test_ietf/test_ietf_ark.py @@ -48,7 +48,7 @@ def test_ietf_ark(curve_variant, file_prefix, subdir, gamma_len): salt = bytes.fromhex(vector.get("salt", "")) # Public Key check - pk_bytes = TinyVRF[curve_variant].get_public_key(secret_scalar) + pk_bytes = curve_variant.public_key_from_secret(secret_scalar) public_key = curve_variant.string_to_point(pk_bytes) assert public_key.point_to_string().hex() == vector["pk"] @@ -58,8 +58,8 @@ def test_ietf_ark(curve_variant, file_prefix, subdir, gamma_len): assert input_point.point_to_string().hex() == vector["h"] proof = TinyVRF[curve_variant].prove(alpha, secret_scalar, additional_data, salt) - proof_bytes = proof.to_bytes() - proof_rt = TinyVRF[curve_variant].from_bytes(proof_bytes) + proof_bytes = proof.encode() + proof_rt = TinyVRF[curve_variant].decode(proof_bytes) # Proof components check gamma = proof_bytes[:gamma_len] @@ -76,7 +76,7 @@ def test_ietf_ark(curve_variant, file_prefix, subdir, gamma_len): assert TinyVRF[curve_variant].ecvrf_proof_to_hash(proof_bytes).hex() == vector["beta"] assert proof.verify(pk_bytes, alpha, additional_data, salt) - assert proof_rt.to_bytes() == proof_bytes + assert proof_rt.encode() == proof_bytes assert proof_rt.verify(pk_bytes, alpha, additional_data, salt) if not found: diff --git a/tests/test_ietf/test_ietf_base.py b/tests/test_ietf/test_ietf_base.py index b5bb5da..e180152 100644 --- a/tests/test_ietf/test_ietf_base.py +++ b/tests/test_ietf/test_ietf_base.py @@ -50,17 +50,17 @@ def test_ietf_base(curve_variant, file_prefix): additional_data = bytes.fromhex(vector["ad"]) # Public Key - pk_bytes = TinyVRF[curve_variant].get_public_key(secret_scalar) + pk_bytes = curve_variant.public_key_from_secret(secret_scalar) proof = TinyVRF[curve_variant].prove(alpha, secret_scalar, additional_data) - proof_bytes = proof.to_bytes() - proof_rt = TinyVRF[curve_variant].from_bytes(proof_bytes) + proof_bytes = proof.encode() + proof_rt = TinyVRF[curve_variant].decode(proof_bytes) # Verify verified = proof.verify(pk_bytes, alpha, additional_data) assert verified, f"Proof Verification Failed for {file} vector {j}" - assert proof_rt.to_bytes() == proof_bytes + assert proof_rt.encode() == proof_bytes assert proof_rt.verify(pk_bytes, alpha, additional_data) if not found: diff --git a/tests/test_keygen.py b/tests/test_keygen.py index e361cc9..7cce882 100644 --- a/tests/test_keygen.py +++ b/tests/test_keygen.py @@ -1,7 +1,6 @@ import pytest -from dot_ring import Bandersnatch, secret_from_seed -from dot_ring.vrf.ietf import TinyVRF +from dot_ring import Bandersnatch @pytest.mark.parametrize( @@ -20,19 +19,19 @@ ], ) def test_secret_from_seed_vectors(seed: int, expected_pk: str, expected_sk: str) -> None: - pk, sk = secret_from_seed(seed.to_bytes(32, "little"), Bandersnatch) + pk, sk = Bandersnatch.secret_from_seed(seed.to_bytes(32, "little")) assert pk.hex() == expected_pk assert sk.hex() == expected_sk def test_secret_from_seed_public_key_roundtrip() -> None: seed = (2**32 - 1).to_bytes(32, "little") - pk, sk = secret_from_seed(seed, Bandersnatch) - assert pk == TinyVRF[Bandersnatch].get_public_key(sk) + pk, sk = Bandersnatch.secret_from_seed(seed) + assert pk == Bandersnatch.public_key_from_secret(sk) def test_secret_from_seed_type_errors() -> None: with pytest.raises(TypeError): - secret_from_seed("not-bytes") # type: ignore[arg-type] + Bandersnatch.secret_from_seed("not-bytes") # type: ignore[arg-type] with pytest.raises(TypeError): - secret_from_seed(b"\x00" * 32, "not-a-curve") # type: ignore[arg-type] + Bandersnatch.public_key_from_secret("not-bytes") # type: ignore[arg-type] diff --git a/tests/test_pedersen/test_pedersen_ark.py b/tests/test_pedersen/test_pedersen_ark.py index 24a95f5..6ad5c2e 100644 --- a/tests/test_pedersen/test_pedersen_ark.py +++ b/tests/test_pedersen/test_pedersen_ark.py @@ -46,7 +46,7 @@ def test_pedersen_ietf(curve_variant, file_prefix, subdir): additional_data = bytes.fromhex(vector["ad"]) # Public Key check - pk_bytes = PedersenVRF[curve_variant].get_public_key(secret_scalar) + pk_bytes = curve_variant.public_key_from_secret(secret_scalar) # Input Point check input_point = curve_variant.encode_to_curve(alpha) @@ -54,8 +54,8 @@ def test_pedersen_ietf(curve_variant, file_prefix, subdir): assert input_point.point_to_string().hex() == vector["h"] proof = PedersenVRF[curve_variant].prove(alpha, secret_scalar, additional_data) - proof_bytes = proof.to_bytes() - proof_rt = PedersenVRF[curve_variant].from_bytes(proof_bytes) + proof_bytes = proof.encode() + proof_rt = PedersenVRF[curve_variant].decode(proof_bytes) assert pk_bytes.hex() == vector["pk"], "Invalid Public Key" assert proof.output_point.point_to_string().hex() == vector["gamma"] @@ -76,7 +76,7 @@ def test_pedersen_ietf(curve_variant, file_prefix, subdir): assert PedersenVRF[curve_variant].ecvrf_proof_to_hash(proof.output_point.point_to_string()).hex() == vector["beta"] assert proof.verify(alpha, additional_data) - assert proof_rt.to_bytes() == proof_bytes + assert proof_rt.encode() == proof_bytes assert proof_rt.verify(alpha, additional_data) if not found: diff --git a/tests/test_pedersen/test_pedersen_base.py b/tests/test_pedersen/test_pedersen_base.py index 6cad1b8..c1ef9ab 100644 --- a/tests/test_pedersen/test_pedersen_base.py +++ b/tests/test_pedersen/test_pedersen_base.py @@ -54,13 +54,13 @@ def test_pedersen_base(curve_variant, file_prefix, slice_end): curve_variant.encode_to_curve(alpha) proof = PedersenVRF[curve_variant].prove(alpha, secret_scalar, additional_data) - proof_bytes = proof.to_bytes() - proof_rt = PedersenVRF[curve_variant].from_bytes(proof_bytes) + proof_bytes = proof.encode() + proof_rt = PedersenVRF[curve_variant].decode(proof_bytes) verified = proof.verify(alpha, additional_data) assert verified, f"Proof Verification Failed for {file} vector {j}" - assert proof_rt.to_bytes() == proof_bytes + assert proof_rt.encode() == proof_bytes assert proof_rt.verify(alpha, additional_data) if not found: diff --git a/tests/test_ring_vrf/test_audit_regressions.py b/tests/test_ring_vrf/test_audit_regressions.py index 6ecb64c..0e14986 100644 --- a/tests/test_ring_vrf/test_audit_regressions.py +++ b/tests/test_ring_vrf/test_audit_regressions.py @@ -9,7 +9,7 @@ def _keys(count: int) -> list[bytes]: - return [RingVRF[Bandersnatch].get_public_key((i + 1).to_bytes(32, "little")) for i in range(count)] + return [Bandersnatch.public_key_from_secret((i + 1).to_bytes(32, "little")) for i in range(count)] def _point_tuple(key: bytes) -> tuple[int, int]: @@ -20,7 +20,7 @@ def _point_tuple(key: bytes) -> tuple[int, int]: def _proof_fixture(): sk = bytes.fromhex("01" * 32) - pk = RingVRF[Bandersnatch].get_public_key(sk) + pk = Bandersnatch.public_key_from_secret(sk) params = RingProofParams(test_vectors=True) ring = Ring([pk, *_keys(7)[1:]], params) ring_root = RingRoot.from_ring(ring, params) @@ -57,45 +57,45 @@ def test_ring_keys_pad_decode_failures_in_place(): assert ring.nm_points[3] == _point_tuple(pk2) -def test_ring_vrf_from_bytes_rejects_trailing_bytes(): +def test_ring_vrf_decode_rejects_trailing_bytes(): _, _, _, ring_root, proof = _proof_fixture() with pytest.raises(ValueError, match="Ring VRF proof must be exactly"): - RingVRF[Bandersnatch].from_bytes(proof.to_bytes() + b"junk") + RingVRF[Bandersnatch].decode(proof.encode() + b"junk") with pytest.raises(ValueError, match="ring root must be exactly"): - RingRoot.from_bytes(ring_root.to_bytes() + b"junk") + RingRoot.decode(ring_root.encode() + b"junk") -def test_ring_root_from_bytes_cache_returns_fresh_root(): +def test_decode_ring_root_cache_returns_fresh_root(): _, _, _, ring_root, _ = _proof_fixture() - root_bytes = ring_root.to_bytes() + root_bytes = ring_root.encode() - parsed1 = RingRoot.from_bytes(root_bytes, ring_root.params) - parsed2 = RingRoot.from_bytes(root_bytes, ring_root.params) + parsed1 = RingRoot.decode(root_bytes, ring_root.params) + parsed2 = RingRoot.decode(root_bytes, ring_root.params) - assert parsed1.to_bytes() == root_bytes - assert parsed2.to_bytes() == root_bytes + assert parsed1.encode() == root_bytes + assert parsed2.encode() == root_bytes assert parsed1 is not parsed2 assert parsed1.px.commitment is not parsed2.px.commitment -def test_pedersen_from_bytes_rejects_noncanonical_scalars(): +def test_pedersen_decode_rejects_noncanonical_scalars(): _, _, _, _, proof = _proof_fixture() - pedersen_bytes = bytearray(proof.pedersen_proof.to_bytes()) + pedersen_bytes = bytearray(proof.pedersen_proof.encode()) scalar_offset = 4 * point_len(Bandersnatch) s = int.from_bytes(pedersen_bytes[scalar_offset : scalar_offset + 32], "little") pedersen_bytes[scalar_offset : scalar_offset + 32] = (s + Bandersnatch.curve.params.subgroup_order).to_bytes(32, "little") with pytest.raises(ValueError, match="not canonical"): - PedersenVRF[Bandersnatch].from_bytes(bytes(pedersen_bytes)) + PedersenVRF[Bandersnatch].decode(bytes(pedersen_bytes)) with pytest.raises(ValueError, match="not canonical"): - RingVRF[Bandersnatch].from_bytes(bytes(pedersen_bytes) + proof.to_bytes()[len(pedersen_bytes) :]) + RingVRF[Bandersnatch].decode(bytes(pedersen_bytes) + proof.encode()[len(pedersen_bytes) :]) def test_prove_rejects_producer_key_that_does_not_match_secret(): sk1 = bytes.fromhex("01" * 32) - pk1 = RingVRF[Bandersnatch].get_public_key(sk1) - pk2 = RingVRF[Bandersnatch].get_public_key(bytes.fromhex("02" * 32)) + pk1 = Bandersnatch.public_key_from_secret(sk1) + pk2 = Bandersnatch.public_key_from_secret(bytes.fromhex("02" * 32)) params = RingProofParams(test_vectors=True) ring = Ring([pk1, pk2], params) ring_root = RingRoot.from_ring(ring, params) @@ -114,7 +114,7 @@ def test_ring_root_builder_caches_root_when_full(): built_root = builder.finalize() direct_root = context.ring_root(keys) - assert built_root.to_bytes() == direct_root.to_bytes() + assert built_root.encode() == direct_root.encode() assert builder.finalize() is built_root with pytest.raises(ValueError, match="too many keys"): builder.push(keys[0]) diff --git a/tests/test_ring_vrf/test_ring_vrf.py b/tests/test_ring_vrf/test_ring_vrf.py index 9016daa..750fb85 100644 --- a/tests/test_ring_vrf/test_ring_vrf.py +++ b/tests/test_ring_vrf/test_ring_vrf.py @@ -29,18 +29,18 @@ def test_ring_proof(): ring_time = time.time() print(f"\nTime taken for Ring Root Construction: \t\t {ring_time - start_time} seconds") - p_k = RingVRF[Bandersnatch].get_public_key(s_k) + p_k = Bandersnatch.public_key_from_secret(s_k) ring_vrf_proof = RingVRF[Bandersnatch].prove(alpha, ad, s_k, p_k, ring, ring_root) - proof_bytes = ring_vrf_proof.to_bytes() - proof_rt = RingVRF[Bandersnatch].from_bytes(proof_bytes) + proof_bytes = ring_vrf_proof.encode() + proof_rt = RingVRF[Bandersnatch].decode(proof_bytes) end_time = time.time() print(f"Time taken for Ring VRF Proof Generation: \t {end_time - ring_time} seconds") assert p_k.hex() == item["pk"], "Invalid Public Key" - assert ring_root.to_bytes().hex() == item["ring_pks_com"], "Invalid Ring Root" + assert ring_root.encode().hex() == item["ring_pks_com"], "Invalid Ring Root" assert ( - ring_vrf_proof.to_bytes().hex() + ring_vrf_proof.encode().hex() == item["gamma"] + item["proof_pk_com"] + item["proof_r"] + item["proof_ok"] + item["proof_s"] + item["proof_sb"] + item["ring_proof"] ), "Unexpected Proof" start = time.time() @@ -50,7 +50,7 @@ def test_ring_proof(): time.time() - start, " seconds", ) - assert proof_rt.to_bytes() == proof_bytes + assert proof_rt.encode() == proof_bytes assert proof_rt.verify(alpha, ad, ring, ring_root) print(f"✅ Testcase {index + 1} of {os.path.basename(file_path)}") diff --git a/tests/test_vectors.py b/tests/test_vectors.py index 51301f3..6e23293 100644 --- a/tests/test_vectors.py +++ b/tests/test_vectors.py @@ -328,8 +328,8 @@ def test_wrong_public_key_ietf(self): sk1 = bytes.fromhex("0101010101010101010101010101010101010101010101010101010101010101") sk2 = bytes.fromhex("0202020202020202020202020202020202020202020202020202020202020202") - pk1 = TinyVRF[Bandersnatch].get_public_key(sk1) - pk2 = TinyVRF[Bandersnatch].get_public_key(sk2) + pk1 = Bandersnatch.public_key_from_secret(sk1) + pk2 = Bandersnatch.public_key_from_secret(sk2) alpha = b"test_input" ad = b"test_ad" @@ -346,7 +346,7 @@ def test_wrong_public_key_ietf(self): def test_wrong_input_ietf(self): """IETF VRF verification should fail with wrong input.""" sk = bytes.fromhex("0101010101010101010101010101010101010101010101010101010101010101") - pk = TinyVRF[Bandersnatch].get_public_key(sk) + pk = Bandersnatch.public_key_from_secret(sk) alpha1 = b"correct_input" alpha2 = b"wrong_input" @@ -364,7 +364,7 @@ def test_wrong_input_ietf(self): def test_wrong_ad_ietf(self): """IETF VRF verification should fail with wrong additional data.""" sk = bytes.fromhex("0101010101010101010101010101010101010101010101010101010101010101") - pk = TinyVRF[Bandersnatch].get_public_key(sk) + pk = Bandersnatch.public_key_from_secret(sk) alpha = b"test_input" ad1 = b"correct_ad" @@ -399,18 +399,18 @@ def test_wrong_input_pedersen(self): def test_wrong_ring_root(self): """Ring VRF verification should fail with wrong ring root.""" sk = bytes.fromhex("0101010101010101010101010101010101010101010101010101010101010101") - pk = RingVRF[Bandersnatch].get_public_key(sk) + pk = Bandersnatch.public_key_from_secret(sk) # Create two different rings ring1 = [pk] for i in range(7): other_sk = (i + 2).to_bytes(32, "little") - ring1.append(RingVRF[Bandersnatch].get_public_key(other_sk)) + ring1.append(Bandersnatch.public_key_from_secret(other_sk)) ring2 = [pk] for i in range(7): other_sk = (i + 100).to_bytes(32, "little") - ring2.append(RingVRF[Bandersnatch].get_public_key(other_sk)) + ring2.append(Bandersnatch.public_key_from_secret(other_sk)) alpha = b"test_input" ad = b"test_ad" @@ -476,12 +476,12 @@ def test_ring_nondeterministic(self): but both must still verify correctly. """ sk = bytes.fromhex("0101010101010101010101010101010101010101010101010101010101010101") - pk = RingVRF[Bandersnatch].get_public_key(sk) + pk = Bandersnatch.public_key_from_secret(sk) ring_keys = [pk] for i in range(7): other_sk = (i + 2).to_bytes(32, "little") - ring_keys.append(RingVRF[Bandersnatch].get_public_key(other_sk)) + ring_keys.append(Bandersnatch.public_key_from_secret(other_sk)) alpha = b"deterministic_test" ad = b"test_ad" @@ -495,7 +495,7 @@ def test_ring_nondeterministic(self): proof2 = RingVRF[Bandersnatch].prove(alpha, ad, sk, pk, ring, ring_root) # Proof bytes should differ due to random blinding - assert proof1.to_bytes() != proof2.to_bytes(), "Ring proofs should be non-deterministic with random ZK-row blinding" + assert proof1.encode() != proof2.encode(), "Ring proofs should be non-deterministic with random ZK-row blinding" # Both proofs must still verify assert proof1.verify(alpha, ad, ring, ring_root), "First proof verification failed" diff --git a/tests/test_verify_ring_sig.py b/tests/test_verify_ring_sig.py index 7e5cebf..5c1e58b 100644 --- a/tests/test_verify_ring_sig.py +++ b/tests/test_verify_ring_sig.py @@ -23,20 +23,6 @@ ) -def add_bandersnatch_points( - point1: tuple[int | None, int | None], - point2: tuple[int | None, int | None], -) -> tuple[int | None, int | None]: - def to_point(point: tuple[int | None, int | None]): - x, y = point - if x is None or y is None: - return Bandersnatch.identity() - return Bandersnatch.point(int(x), int(y)) - - result = to_point(point1) + to_point(point2) - return result.x, result.y - - def parse_proof_from_json(proof_json: dict) -> tuple: proof = proof_json["proof"] @@ -119,8 +105,9 @@ def verify_vector(vector_path: Path) -> None: result_y_bytes = bytes.fromhex(params["result"]["y"]) result_point = deserialize_bandersnatch_point(result_x_bytes, result_y_bytes) - result_ark_bytes = result_x_bytes + result_y_bytes - result_plus_seed = add_bandersnatch_points(result_point, seed_point) + result_point = Bandersnatch.point(result_point) + seed_point = Bandersnatch.point(seed_point) + result_plus_seed = result_point + seed_point proof_tuple, raw_bytes = parse_proof_from_json(proof_data) vk_bytes, fixed_cols = parse_verifier_key(proof_data["verifier_key"]["verification_key"]) @@ -161,7 +148,7 @@ def verify_vector(vector_path: Path) -> None: verifier = Verify( proof=proof_tuple, fixed_cols=fixed_cols, - relation_to_prove=result_ark_bytes, + relation_to_prove=result_point, result_plus_seed=result_plus_seed, seed_point=seed_point, domain=domain, diff --git a/tests/vectors/ark-vrf/bandersnatch_sw_sha-512_tai_ring.json b/tests/vectors/ark-vrf/bandersnatch_sw_sha-512_tai_ring.json deleted file mode 100644 index 790e0cf..0000000 --- a/tests/vectors/ark-vrf/bandersnatch_sw_sha-512_tai_ring.json +++ /dev/null @@ -1,135 +0,0 @@ -[ - { - "comment": "bandersnatch_sw_sha-512_tai_ring - vector-1", - "sk": "a34b2e0e25e0c94e46558248327a46ffc6a6e9abeffafa531674da7761f1c700", - "pk": "ea19916051e8f699176441e636a03eac86501bfb13b520a3e0f24fd3d6ecec2580", - "alpha": "", - "ad": "", - "h": "aa1a649e5ec9bb921ef7828fc239f80183027bfd07280737edf6f04974c6750200", - "gamma": "286e552e9d1026c501e44856b4f3aa96957f32ed5e34c2e03cee05426ee4486900", - "beta": "7178a83ea9979546bfbceb7bb125d07e74397e133a2dc23ba126d706f9e8c084", - "blinding": "101ce60d21f5c6f590ddef98a4bf7f476baca00564542ac41838459e1bd3b816", - "proof_pk_com": "4b3349f30ffd34496951344d9c312dc2f9d656115c258ddcb74fdf84594c243900", - "proof_r": "802c8905555241465e7b7a9d4888373ee6e01fac23528b429cdef9bc12f3c93000", - "proof_ok": "b028f7c1966914ae3c9439c43a60d8b09278526a8e3ef5bca46217d55791fe4a80", - "proof_s": "a482ef87b37ece01ae3acd22a4074db24243db8ede369a0d926656f8f95c2b04", - "proof_sb": "d7db22d81aa9f66a9bc5554c48d972de3dc162e50f42376acbbb7ad7209f4016", - "ring_pks": "2a33f0ef115c00a4cc79a18415af4b67ad74d17493dbd8f62112389a300cdb52001f547e91c60502ff20f353bfcf5f163d13ea92a84ed1e26bc3e2538c5661844c000510bf087970c1f4b1971bccfdf7e08d9602b5a0c9409b324e5c75d24aad860980ea19916051e8f699176441e636a03eac86501bfb13b520a3e0f24fd3d6ecec2580da46d89c4957bf836505c3bc5690ada8a827bbc6fa68781c73f9f4713493282f0025863c6a761ec1891e20097a7079774015685214eecbcb976eef51cc209f0268009b54e3c8b37027dfd7bb0f223d2523037775b7610214d8ca18b638755a759d5d0050a0ccb4e59c65b4972c806e850f53ae4d7c9307852e2b3858e32ed2ea1a0a0980", - "ring_pks_com": "82ec7c0c76dd39b37b85002cfa00056fcbf9a9a54890c8f1103c939c5eea01bc1c21784256c99c87ebe853bee37ccd6696807828f1fb40ad5b211ef6b8e124eebccd1898f9c1c586737a96754dec8dcfa2c092e33a7d5878065c681d7a69dcdb92e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf", - "ring_proof": "b15de96be694ed3587bca8dc9c414e16bd26a96aa797051187d70dffd004740cc874a5ddaf0c3a80bf24a51b68a90fef9107bd20fe94a01157764aab5f300d7e2fcba2178cb80851890a656d89550d0bebf60cca8c23575011d2f37cdc06dcdd8ba010b8fcc764fc8613b1db8d2853b9a0e69a20df78949de07ccc2ad2770f14cc314b5837fd1b5cdb5b52b8fbe0e92aa5d4c8752e24929871c003937a6c43d403ba4e9b0c9c569a0a0c1a7402130d5842ce9eb49f578a3911c899b8e16fb7455c60bcaa69ef629730be1945f95ab2479661b14b00d54fa63e030f0cf9e17e07df3b9a6c5e6aebbe823efa36410ef6f53578620244eaf4b3790521362131835e248e86bc2fa12be36a40a280c30fe14bd950021f49b313f571ff54568f07c54dad119c84e98dfc07afefffcc7f7bee87e78eb998386afc3ad4bf73a0bc81f01a2038031be50e260411406e3b40563e71d78981eb573aee87c808662bd6d8b02bec33309ca8b975cde667ce3153e7c8b4ec7aaae30ff4feeb2ae0804489df446711c79a33463d42b4cbeb14abc977e1c9204e77aa7028d953a6cedccf0715cd0e92906b69816feb41b0cd9083bc880ca7ec855ffd924fe46241b438150e62a107523c1f095c1a0fcee9387dcdcc251ba0153ef022f7c30e28eb037e1808ff514c872070be6cf6120055d8b551321036658a6a6be3db9a8040fd914490b7ac00a0a7e486638835f0ce3965f32ecf8ae88b2ebd392ec9429defbbe77798e2bb588aaf9b747ca8f670e41ea70db736fa49ca5e6cffc3d51a4770172189277a8a105a7a50d8f55cfc46b268e32ca4fdd23275" - }, - { - "comment": "bandersnatch_sw_sha-512_tai_ring - vector-2", - "sk": "930516bb9a8188741111f0c34f6996913ad705abc614831cb092c1a09dbbf919", - "pk": "7b7433d589da0d7c86ac0be95779aebd39b557c88253c759bb1f11491494bf1480", - "alpha": "0a", - "ad": "", - "h": "5b678275c3b8c968df733aa1d908f655f4fb283f766c579cfb6923312380eb0480", - "gamma": "229a28f9e93569ab142bbdaf4621fb05c148aba2072768a3c2aad7e4bf55550f00", - "beta": "1face71acd5f4f4a97295fee1b0eab3b66f2d27db1d5f4da5628b7666c4b09d9", - "blinding": "3a2cad30984bd4f7f87867a26925361dcd66317bd503298e3abed37774aad108", - "proof_pk_com": "a85601fa16306e57062208f6955390e2a0b046994274786d3c2bea6abde4380400", - "proof_r": "00d5981b5b24ec1bee09563d904304f111bdaf4eeda9144b0aca8ecfe0a7430380", - "proof_ok": "33cd57b070427bdadebd115fb04f2c5dd6b7985eb7fcc2744d744d28772c831100", - "proof_s": "e28ed027aa4f90b503a741b8bb3d92be9aabd38b2688566603a8cb5ff7ba4113", - "proof_sb": "a1b3aca030b593e5c5097566d584ecaeeb9f72a5dc010deae18e66f866b27204", - "ring_pks": "2a33f0ef115c00a4cc79a18415af4b67ad74d17493dbd8f62112389a300cdb52001f547e91c60502ff20f353bfcf5f163d13ea92a84ed1e26bc3e2538c5661844c000510bf087970c1f4b1971bccfdf7e08d9602b5a0c9409b324e5c75d24aad8609807b7433d589da0d7c86ac0be95779aebd39b557c88253c759bb1f11491494bf1480da46d89c4957bf836505c3bc5690ada8a827bbc6fa68781c73f9f4713493282f0025863c6a761ec1891e20097a7079774015685214eecbcb976eef51cc209f0268009b54e3c8b37027dfd7bb0f223d2523037775b7610214d8ca18b638755a759d5d0050a0ccb4e59c65b4972c806e850f53ae4d7c9307852e2b3858e32ed2ea1a0a0980", - "ring_pks_com": "8f3123b2261e4a09508ff8ee33256cf243d6db0451c6ef90ddf82b4470092214626f9564968507e9778313110ae58c7390cb472265c709870ef0c819ba9e24d27ec037a37d6dfec4f9a2ea36bdf335f758549096f65406d3442c8673bb95c0c292e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf", - "ring_proof": "8a4a091855e35f3661319ec351cc6f1048bb151bd3cc3ee9fc49d063653a01cbc95b165db39695408652ec4e904657d99107bd20fe94a01157764aab5f300d7e2fcba2178cb80851890a656d89550d0bebf60cca8c23575011d2f37cdc06dcddabb53ef3435264067b5e5252ad348a2c3232a171c4a4c2730b5fac0cb66786251e953e41fd22c7c56b2e9dc69a524d008bb3d5f011a22839518ff2ed7593951b651cc0b65f4cb12903e4dbb27253d893c81fef82bce7bd130b48e2b30b32828fbbee9a4423e229da282d97ca3d99e4c611500c3c6195bceb83adaa558b61ee0a1265dda98fdcd568aeb69529e686f36a649cb7cc13e8974eb3bfdfd9f4b56d086290eb4628ab4deaf50bbfb1e5a4d685c97fd68c7acb4d6c0ea074dd57122a4c06bef45b13f97fa8ec7893696c000129844d31f220be2d6ec1ea2e2e8482514a3af99357d7012bf668617e9cb6a32e4e56081ab46f6abe2c523f72842eb7103d278e8d617aed40de9db6083ed3f8539e7ecd7373cef0efbc82d01afb34a844278080b63b0e4275b70b82a524c45710a0c691d6b5147f516e279f4d7394e56d55b84db600e789cf473720f501faee2f00d06eee7dc9caf49a4884e41a6285b534c79e54b7668cc1eeb1147e5035836108eee02524669b4738f5e406b797e7713eb6d994dfee6e3bc08ebc6d7d8f83d06e97d8903ab4bd5333bb6b7814879d635554207c9c6bdfa7e20ce0068705154b6cf6530562e12da160e164c396a766222f826de67f43114b1d59d6c48a5753aa30e54ce01d3794660829a0810a924ac7c8ef6973d77c5afdbc3f48291886220469" - }, - { - "comment": "bandersnatch_sw_sha-512_tai_ring - vector-3", - "sk": "162578407cef66316bc4f8b37acd8ffdf8270acd9817c5373bd66779a20ecf1b", - "pk": "5f08b092890bed60b015be1e228d515d1bcf83fe254ae8f2c8621b53e33c836a00", - "alpha": "", - "ad": "0b8c", - "h": "aa1a649e5ec9bb921ef7828fc239f80183027bfd07280737edf6f04974c6750200", - "gamma": "4faf6e1431a078c14be293a344d58067b7bdf62999bddeff7c843145e038522f80", - "beta": "445e8b807d95e09c147b0761a46ff84758f145fa467913e57c0b209e7d744abf", - "blinding": "f06965d9912d89aa26b24de126d9d72519308ce4b458f58d0f27d55b59ff8c0e", - "proof_pk_com": "1b3834e85c6c1edb035b73d1c62fa8219a6e6b39fbf7619b69e261f02bc93d5080", - "proof_r": "39c2ba376b065b8fd89fc4d89bdd8b52073f5f4fc35fe8f67ee83d73d1f7a34200", - "proof_ok": "eec00f5462c3acc58cdc4210e770b12830a3a29b390fad3edbeb6ec6af3b373e80", - "proof_s": "4b7bf40c6514edfa8b15dba03bf2f96e4f8b32ea56faf69e9f10e4ff01bcac06", - "proof_sb": "79bbfda713a59183dddbf4744cc67e3f9c3e4aaf8fdb7a34c7a17d818e37cb03", - "ring_pks": "2a33f0ef115c00a4cc79a18415af4b67ad74d17493dbd8f62112389a300cdb52001f547e91c60502ff20f353bfcf5f163d13ea92a84ed1e26bc3e2538c5661844c000510bf087970c1f4b1971bccfdf7e08d9602b5a0c9409b324e5c75d24aad8609805f08b092890bed60b015be1e228d515d1bcf83fe254ae8f2c8621b53e33c836a00da46d89c4957bf836505c3bc5690ada8a827bbc6fa68781c73f9f4713493282f0025863c6a761ec1891e20097a7079774015685214eecbcb976eef51cc209f0268009b54e3c8b37027dfd7bb0f223d2523037775b7610214d8ca18b638755a759d5d0050a0ccb4e59c65b4972c806e850f53ae4d7c9307852e2b3858e32ed2ea1a0a0980", - "ring_pks_com": "a09e4172efad8849b48b4040d514438735521b53d9b6201a87448f45b71bdbbc9af4b36f0ad3d81f7ee4f8e6bf4afc948ea71fb87ff63cafbfeef864d7a314b71d2633bf092aa6d28a16df81b98a013b627f27499d80608409176df25a5014e192e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf", - "ring_proof": "af22e9bf883f46fd330815ae1b735054c4cd1b6b910bef436a66812038de2436198a52f0f8b89e66bcd6c0554d903c389107bd20fe94a01157764aab5f300d7e2fcba2178cb80851890a656d89550d0bebf60cca8c23575011d2f37cdc06dcdd95cd797a54156f5e2ed94d66f8785c0c7c01688337fef8b42d11695904c96dec1024df34b9e04703888101939f85f56b8e7c357f6bb524b91106f8e27db467eba6811ff8d52651c778aa545fa166ee3b9c6fdc5d147503afb3cf432a6fe81db08f13b6e8b5fc03ac16876fc638b7d7fe9cfa575728da16105cb4df239ee0a20fd3be4b59abcc9a74ecb0dcd99b30d97f48ce4f38983c513ecfb7bc2eda62b965b4a67036d0ea0d2c1e82f2f9a461f3ea2c9f4c9ae68043fc7e0eb6b72aec980c48e440472cb07ada0de61718e0c75d2612745125831c0b28d5718376dbcd4f6bb6381fcabb6b0705055db5111e665e91dc1f3b91bda4e2a230046efa9f3c286f1a5a3221bf80f0ac83fe5b5e3e9c6e087e2875e185096ddece39d9ccdaefee5172683871783453c25fb994359ffea2ee1f993d822b5c4f0c5291dc627f55cd0bb1e1b6a200e296a0264ee391c9559b2f748f8a321025ab231aa195ed1b0e15c44085b3558ab68d08f4f86c2e37a911da76f4bf09390ce1a17d6c399d0cbebac08da37a89b6925810204e3f3aa80a2b52b5c493fdabc4372e463381b4ce8a7df9ec85e9d9f116202699ec410d80a95978de1bbf2a506a876908401e867727b61898e6b475a7f174fc3fe7baf04d1dac31d125f9baffc6a5b9884265487d67a27749c3dc44915a1f61fe983fd94bc6c7bb" - }, - { - "comment": "bandersnatch_sw_sha-512_tai_ring - vector-4", - "sk": "17a1ff6e2f640390c0daf68df221212b79f5d5b8c88a610b4ce26489af066b01", - "pk": "5a6d13e3cd5c95285ac88c1e09125426469ed180b6d5e7d63c1b1f49f8cb877000", - "alpha": "73616d706c65", - "ad": "", - "h": "f470cdec5856dc89de0714ad58c34fdaf665a9be69f40e3f3ea11f5df7f3343380", - "gamma": "aefd62fa4370d591105083e066f18ea9c1385b2108ebd8218052f821686fe14480", - "beta": "8cd4da8facab33febd81a501bfd944db3afdec1ba4c5e7015a79851a834b3a4a", - "blinding": "d197c4d1733d7fd97afb6d8e9d18d61b470e2818a52124aee01e8196a3770615", - "proof_pk_com": "fd70ec5877d0446e5cc9364dad6c2d2d4172b039226ccc74f98a208b8cc78d2800", - "proof_r": "ac535a8c53c7e33fd72116b9c64cce71158e6f9cf15bfe9c574c4a572a04ff4480", - "proof_ok": "9c1555853acb686809d7827afebf6de9c5c98312436410afc272ebb868d8aa5600", - "proof_s": "41a987008d013ebd10030ca5994d85cd8bd957f2940c9c3d015342315b3d4700", - "proof_sb": "573aeaea8072de85eef603a0c44c7f07ec212086cbb91b672e2456fe4a218809", - "ring_pks": "2a33f0ef115c00a4cc79a18415af4b67ad74d17493dbd8f62112389a300cdb52001f547e91c60502ff20f353bfcf5f163d13ea92a84ed1e26bc3e2538c5661844c000510bf087970c1f4b1971bccfdf7e08d9602b5a0c9409b324e5c75d24aad8609805a6d13e3cd5c95285ac88c1e09125426469ed180b6d5e7d63c1b1f49f8cb877000da46d89c4957bf836505c3bc5690ada8a827bbc6fa68781c73f9f4713493282f0025863c6a761ec1891e20097a7079774015685214eecbcb976eef51cc209f0268009b54e3c8b37027dfd7bb0f223d2523037775b7610214d8ca18b638755a759d5d0050a0ccb4e59c65b4972c806e850f53ae4d7c9307852e2b3858e32ed2ea1a0a0980", - "ring_pks_com": "a1d78bc514ff88ebe235a580ce406f482aa98ef3efde2055c41a1cf7e7ea7860fe2c2ac9bb263e79370db0eb899937ff9304664aa1bd3a2ce06fb1683068481a28ccfbb77250eb80c959c9d0d6f477f7c403f54f168f972ebd6c805960ee34a492e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf", - "ring_proof": "8e9177efdb113ef2d96a56fb65e57ddf8a520150b227a649f0b7f5bf754b0c9f63709f8f6f3b7c35a9f658c2aaa6350d9107bd20fe94a01157764aab5f300d7e2fcba2178cb80851890a656d89550d0bebf60cca8c23575011d2f37cdc06dcddb5efc1fc797b375945197f9e2e75afbcfa3457611f8da81900461332000fd7dde8ddf42b9bc493256926e390b2f2a8aaa9ed78d7c9ac6a5d8241b92a9f34b84fc1e315806ca43cb95066125837b89eadb18e695405442848075cd717f742e0567cd55f899aa3a072896f2e52d43a1eabeb0df1275e20765bcbc907b5caac1c21525c0a3f988783ab98d71e7ddd90a373fac54099b3ecbd428607c5c52c06631b811bfab93d0f1226633f5653b850a6e3cd4362a4f8a0f28ca7c8613ff48c2508767b267ad55e1dc6d2ffbf84960e3208edbf5cbb2a7db0e247cf576b048c6305130d1e7608bf2b18d6772687ce1f627f6b63259fec4af02b13cc310cab85c7148f3b151e4d277c21e39e9160222f1f00e6479304bdf5e01506b87249b5117253b1f57dd305687e24a29ea297dbd2e99b17ca09da03b7e5d82480939017a85c4cad332cfeaa5be72e118544d8f73301e98ec5fdc49019462d8443e618cbb33e1dea624c5b741b860cddf16efd7224b4d54ad323d0b129e7ce28ba92655acec84d624dc070fdc635b27f7614d225e2116688933f83ff43256a307eeaac719f4f6bc463fee9a007184aa37c59da0bc16a4d315f8f87aefe94cc078636d39bbe16e69162a0fc8cfe086cc61a92cfa8622d7c4c3cfd640fbe4285d005bec6db4089036035b89e6727348b2306ae2b4cd3d822" - }, - { - "comment": "bandersnatch_sw_sha-512_tai_ring - vector-5", - "sk": "fd47a91def04dc1fc7492337ea29740bf5cad047be5fcddd60976e55ab062817", - "pk": "cfe9be2fa584b7387a76d4537f81ec1b8a1210a9aac7ae72c87bdfcb4c0c761700", - "alpha": "42616e646572736e6174636820766563746f72", - "ad": "", - "h": "673218a4b0b407e8f5c02f3593a80c7bf226fd599e023a5135fe056b29eec01a00", - "gamma": "570b39f4c9a058ed6c652e4d94a6020e7afe7b14c58c1e67ce28b5894bd9c24080", - "beta": "c7261d5a43ed02273093e89157198efc3d1d3906038bb43ffbb47d1848833545", - "blinding": "0cf83dce52d08b7798eb9144d912b7b31204fe92896af3e792236be115f76919", - "proof_pk_com": "ac3b3a47430e1a5cbe997df2951e5fe13fed96d613522b9ba385b256191ada5400", - "proof_r": "e53915bcfc3aaf3ffde07b0e9d7fbf7d1d7f34bffb8ab82cb827028c596cc70700", - "proof_ok": "6876624d12db6c30c864fdbacb718ec0e6385108b11539f16bf2d3456316d94500", - "proof_s": "a1b971d14062080f7a5ead1ef8a19877823447faa9fdf34141fd9eac6c10531c", - "proof_sb": "53fd9abae2688ddd003f40a79229b4a24790dd02245e0111e676b39ea5424713", - "ring_pks": "2a33f0ef115c00a4cc79a18415af4b67ad74d17493dbd8f62112389a300cdb52001f547e91c60502ff20f353bfcf5f163d13ea92a84ed1e26bc3e2538c5661844c000510bf087970c1f4b1971bccfdf7e08d9602b5a0c9409b324e5c75d24aad860980cfe9be2fa584b7387a76d4537f81ec1b8a1210a9aac7ae72c87bdfcb4c0c761700da46d89c4957bf836505c3bc5690ada8a827bbc6fa68781c73f9f4713493282f0025863c6a761ec1891e20097a7079774015685214eecbcb976eef51cc209f0268009b54e3c8b37027dfd7bb0f223d2523037775b7610214d8ca18b638755a759d5d0050a0ccb4e59c65b4972c806e850f53ae4d7c9307852e2b3858e32ed2ea1a0a0980", - "ring_pks_com": "a571e8ef956c80ff323656d6ebd4ee9cf3c3dfbe155d1546310b442fc109e7382918bba95df7afb24190b0442b0ba8e9b0043d90e8e2db8ae575ccd57911176c80ba66034460c27e16254fd796b7d3df00e20ff0a509bf92c4c6f53ebe08f23c92e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf", - "ring_proof": "98d740fbf01f88496783222e510cf94145bd8f8b9aea5fdf2b363add9d0fb6ad7a68ab6abe59a91a6f69708783f671389107bd20fe94a01157764aab5f300d7e2fcba2178cb80851890a656d89550d0bebf60cca8c23575011d2f37cdc06dcdda80e66b0614769e66d0693aeae1f81e98bb96a540ece93675e3dce9d5884fd1ee76bccd0402ea6c7d0fd8684fe4f53bbb3e323d5ab16d695eb141ce885cecc2dc595c55422dac72fd4ee68f0042df087696e4b7578096cb22e4cbca6dcb22eb1a2a658e5281e9d1d207cd7f6c6f1a5b05792ed14322c0ec602c0c97a6dc6d12dc42bedebd9a545803d61a8a7ba5bbbb90d20e08dc13ae58413aaa6d803cb076282d8981fa6bc5c1d1119e8ca88562261f5d8d499cef845ff3ce79e33be937b34264023994c699373c7e9c176094a0d8d2c342226de2259a365c345f9d0ec3d3ca6ed14279ecd33b36cadc43c5faf549a60111548f2ac2171256ee7fecb69d15fc0a4c29aec2f93a32420972c746aa0780f2aa6020d5399cf27ba7377905ea252618759fb4b97054c78161b07a365341b0c4434fe2acdc1d59a32a8acbed1c9248bf5e6f9d828a97580378eea4bf16a97293e35ae22d00df6993191a41cb0f64c3691fa59ffc1e8f07772f938d19436c70fde5f7836a5f66d10208128b3f6ec2b403076f4eab2e3cbdcfbeb1a7f62d968b6989fb405cd0e93cd738cb16ce8ae3926f6cff8f5cc07ad8b46aae7bc5dadedaa64c117855d3501af2d89db10b10c0ca1f7ce8182c61f4211b9fc280475cf2c8c6bcdc1c0ff17add695839b5ecc4bcd433e9c0c99b3788da562d9b8937395f1" - }, - { - "comment": "bandersnatch_sw_sha-512_tai_ring - vector-6", - "sk": "fd47a91def04dc1fc7492337ea29740bf5cad047be5fcddd60976e55ab062817", - "pk": "cfe9be2fa584b7387a76d4537f81ec1b8a1210a9aac7ae72c87bdfcb4c0c761700", - "alpha": "42616e646572736e6174636820766563746f72", - "ad": "1f42", - "h": "673218a4b0b407e8f5c02f3593a80c7bf226fd599e023a5135fe056b29eec01a00", - "gamma": "570b39f4c9a058ed6c652e4d94a6020e7afe7b14c58c1e67ce28b5894bd9c24080", - "beta": "c7261d5a43ed02273093e89157198efc3d1d3906038bb43ffbb47d1848833545", - "blinding": "20537ef560d5a8d7483dec55e0b680797376e61d0844bf6971135cd5811ee203", - "proof_pk_com": "1db1d10f797d6208fa724359100b6297c74a98a3f35626ad8dedf891d362ac5780", - "proof_r": "2d6643ffbdc2d4b11024be73349dda34fb1922c7d67a617471bf05ce93f29c1e00", - "proof_ok": "db649772438941f07e1cb4d92ab387aaae22fcb0389d32668a54385a0bd8461580", - "proof_s": "505e46a525c967e840254400b59e75fb40ea21d3c132bcc5032df23bf3dde514", - "proof_sb": "f71544be7ce3ce1a4473d30070c427f84be2f80c080a008c1a4280e40754ff03", - "ring_pks": "2a33f0ef115c00a4cc79a18415af4b67ad74d17493dbd8f62112389a300cdb52001f547e91c60502ff20f353bfcf5f163d13ea92a84ed1e26bc3e2538c5661844c000510bf087970c1f4b1971bccfdf7e08d9602b5a0c9409b324e5c75d24aad860980cfe9be2fa584b7387a76d4537f81ec1b8a1210a9aac7ae72c87bdfcb4c0c761700da46d89c4957bf836505c3bc5690ada8a827bbc6fa68781c73f9f4713493282f0025863c6a761ec1891e20097a7079774015685214eecbcb976eef51cc209f0268009b54e3c8b37027dfd7bb0f223d2523037775b7610214d8ca18b638755a759d5d0050a0ccb4e59c65b4972c806e850f53ae4d7c9307852e2b3858e32ed2ea1a0a0980", - "ring_pks_com": "a571e8ef956c80ff323656d6ebd4ee9cf3c3dfbe155d1546310b442fc109e7382918bba95df7afb24190b0442b0ba8e9b0043d90e8e2db8ae575ccd57911176c80ba66034460c27e16254fd796b7d3df00e20ff0a509bf92c4c6f53ebe08f23c92e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf", - "ring_proof": "ae666c81112ae53e1f1c99a407ec99101fc065e1ec45bafc7e1cc158fdc0ae20fbefdeb7d9ed20753f65413eff532fa79107bd20fe94a01157764aab5f300d7e2fcba2178cb80851890a656d89550d0bebf60cca8c23575011d2f37cdc06dcdda8b466de672aae10748e9da7e61ebf22c3bf1bb99f4deb5918aa57b7c302e82a9d2604cddf28eba6f8bdecb5b56c92f7870f54b5dc6d48a661c04fd7b22ade4370b146d9f9190869487064f2c45e55c3c743d85af87d44c6a325d080116ef6d92660429f5cb98b879281826f2592afc566f6490c5ad636d18e133dcacee76d448d4d4b8220463c692d3470bc7e7eceeeebcff9ad5e6032922d2e2a8d8c003a394546983bd299f72a29c722b56cf100e1c7cc89e720d5c348db1ad365c031715fe9acedf9b345d7b1903ae20e4222128f04603d3aab82389fd6cf10549db6b133cff9f1e3da701353d6984dc95afedb4a44820cb854eaecc841b249909096ce006838dc1b0bc3204a59d3dd83f6feed477de577331f1d36407a8b841dea6f331933484e97e7ac99db464e204b48577a7457f7e6bee3c030ba28fbf1354374ad3eaafee6c6b48b4f62f88ba93cc47b578ee3213a3c5dcfe5fddfc3501d5fc017a3326c89adff61f7a1179eac029eab28a761ab5bf78fcdd75d60e2a6d293cd08178c251f2d1e32e2f32171ee9c7c06b10b9851691aff7e829419758cc6d5fcfa909ffac954c53f2a9c39e0edb1a758d9058908d68e23b7c3d28756bd5138128eed99502b3657cd057e5441f354de23cb26366d1e6db848ce5386372c32ca48203a29753575577163f4a014a0ffdec874be" - }, - { - "comment": "bandersnatch_sw_sha-512_tai_ring - vector-7", - "sk": "f00b6b8332ed0d7d1ca8aaf3a6afcd146b475d43880ff97ebe088ed7d6340601", - "pk": "e690d4b47a99a0434b40217f2228f029a49485874cff87066a822637de41360b00", - "alpha": "42616e646572736e6174636820766563746f72", - "ad": "1f42", - "h": "673218a4b0b407e8f5c02f3593a80c7bf226fd599e023a5135fe056b29eec01a00", - "gamma": "1db8258d742dcbcc1334322338c68d51cfe24301bdc29cd79f8d01c133cd8f1680", - "beta": "99ac81017e84d4cbc8f0b79dcfe3cc7f8899f8e1b8cf71a5194551dd79771d4b", - "blinding": "2cff0f18f6fa445491223aeff1e117c6180a555aa23dd710f1d785c24abf870a", - "proof_pk_com": "caa63e87aed8ca3880a2d63d97dfb9f47c8add13dbf5fc2c80cf410d7c66ed1500", - "proof_r": "ac7737523563fc44092829f1d20a02c50e02380cefc6c871fd32337bf62da15c80", - "proof_ok": "e8a1911928b58371fe29722b83ac3721bb74e85a5a85f549edcaac082293373780", - "proof_s": "0a228726dd7e3350d84d7b6d6e5f8a0b91cf6860d91157f1612df05a2a8f3c0c", - "proof_sb": "87e7717f25017e1855e30decb2bf83029e8a56fa166872ee9e408a581cb61b0a", - "ring_pks": "2a33f0ef115c00a4cc79a18415af4b67ad74d17493dbd8f62112389a300cdb52001f547e91c60502ff20f353bfcf5f163d13ea92a84ed1e26bc3e2538c5661844c000510bf087970c1f4b1971bccfdf7e08d9602b5a0c9409b324e5c75d24aad860980e690d4b47a99a0434b40217f2228f029a49485874cff87066a822637de41360b00da46d89c4957bf836505c3bc5690ada8a827bbc6fa68781c73f9f4713493282f0025863c6a761ec1891e20097a7079774015685214eecbcb976eef51cc209f0268009b54e3c8b37027dfd7bb0f223d2523037775b7610214d8ca18b638755a759d5d0050a0ccb4e59c65b4972c806e850f53ae4d7c9307852e2b3858e32ed2ea1a0a0980", - "ring_pks_com": "a5a7af3800d5c1919ccf4a8824c99534ec21397e976c2181172a4a834d1fed9b2815eb6380b4a5085e032bc83474aee198359675974dd4d9cde6b4c8ac1e4e28750625b3f81a9bd419cd0c6a3ecfe72bdbefaffe2638cca7d0fb9dad25e7e52a92e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf", - "ring_proof": "a199113fa3b5e6bf315f600fe31fd8b9f8aff50d9dbd878e277b3d8f443689fdbebc216e7404e8eecb4b6b2e0487bc219107bd20fe94a01157764aab5f300d7e2fcba2178cb80851890a656d89550d0bebf60cca8c23575011d2f37cdc06dcdd90490cfb5cf225366d8bbaa6520149d47dd3f036a1ebfe01c6d379c96055f4fe23e2b615973a24b7002d57885726b8aba2925f7ec404a7c3321e7cd5e9f1670f8fd3a055f314796ae34146d4db5180f3c9caa95d926203194ac81c6642a81c4943d53a95249952148073ad28303fe0347d9454fa6718524ce568987439af2b45b920fcd0458611ce2926d53be6dee1ab82b38e68b07e30a84be9b8b6a5151024e6887bcec76855b922a540efda026b3ffc19126287bce885343700c43d30780b81c25d75654bd7a67af67f733df137bf6a04d94dc8f795a6a8d0cfba54c8240711307ad6af9fa66395a56e0a3f42825a934b3927d49e5ee4116ae76fe2ea3970403b2bdf89b7a6e6cfa98e1b019259f896c3ac4e62c5b12eea16c3643fbd117048623ecc75b839186abfe0ccbbeb4c89418ab9d0892ec143e1aaba5ede668463849510968f1249929205d68c8151ffd4ebc88fa4d77bad2532611528f3524b10ff3712d132dc35d5aa3dc6f22a78400d2840a3e49ffb065f5f24f50d2d2a00e71e5d91329254e52defb3989836f3a428a44b597276701465f0b75f4eb37ffe3fd4bd1944fcda169bf34efd991f65ad596c4be0b3a0e3d08462bd721ed2292ffd8d3f126ccb7fe6c98b41c02725350133542823a2b688a049f634e98d05481d996a9af47ef340b324604bf2f2d5bc1d01" - } -] \ No newline at end of file diff --git a/tests/vectors/dot-ring/bandersnatch_sw_sha-512_tai_ring.json b/tests/vectors/dot-ring/bandersnatch_sw_sha-512_tai_ring.json deleted file mode 100644 index f475f22..0000000 --- a/tests/vectors/dot-ring/bandersnatch_sw_sha-512_tai_ring.json +++ /dev/null @@ -1,149 +0,0 @@ -[ - { - "comment": "bandersnatch_sw_sha-512_tai_ring - vector-1", - "sk": "a34b2e0e25e0c94e46558248327a46ffc6a6e9abeffafa531674da7761f1c700", - "pk": "ea19916051e8f699176441e636a03eac86501bfb13b520a3e0f24fd3d6ecec2580", - "alpha": "", - "ad": "", - "h": "aa1a649e5ec9bb921ef7828fc239f80183027bfd07280737edf6f04974c6750200", - "gamma": "286e552e9d1026c501e44856b4f3aa96957f32ed5e34c2e03cee05426ee4486900", - "beta": "7178a83ea9979546bfbceb7bb125d07e74397e133a2dc23ba126d706f9e8c084", - "blinding": "101ce60d21f5c6f590ddef98a4bf7f476baca00564542ac41838459e1bd3b816", - "proof_pk_com": "4b3349f30ffd34496951344d9c312dc2f9d656115c258ddcb74fdf84594c243900", - "proof_r": "802c8905555241465e7b7a9d4888373ee6e01fac23528b429cdef9bc12f3c93000", - "proof_ok": "b028f7c1966914ae3c9439c43a60d8b09278526a8e3ef5bca46217d55791fe4a80", - "proof_s": "a482ef87b37ece01ae3acd22a4074db24243db8ede369a0d926656f8f95c2b04", - "proof_sb": "d7db22d81aa9f66a9bc5554c48d972de3dc162e50f42376acbbb7ad7209f4016", - "ring_pks": "84632747d740e4f49a1ea91b988c8c68ccc702907749898ad76efe9086fbbc350006fbd1d78bb5937cc0d46b8f5a8feb683a8b1d6326329c42877ba6e1c41e871b8014390fc91bbb66d6fc0b94663101d7d601b92d8255de190c5b580c4fe9ed596d80ea19916051e8f699176441e636a03eac86501bfb13b520a3e0f24fd3d6ecec258005036bae65fbc539a34a9eaacd2f99d194737c49a86cadc78632ba0ca335582b8092a44c8ad47a5840e863c5eb9430cd320383870a80d4b5878039b756e6edec1b80a2fab54fab6bce5c3c6c8a64e18fdc03144cf27381a3f6a1debcdc7796f16227009b6a628685c087d4f43d102650e4404e4f15281484941480fd09c56bdbd9db2080", - "ring_pks_com": "a9e3f4f6bd0f5859e4cd13beb8e3e51215887d224ead1391e2212c3a2f6f5ca77408cced1a06f339d297c5c9e130c57a98a4149cbb51be61a77942ca39f8c8225b618388c2614ab9c1b1693e4b52e5a41f8ad02407fd18770e2a3fc9dad1b7dc92e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf", - "ring_proof": "b15de96be694ed3587bca8dc9c414e16bd26a96aa797051187d70dffd004740cc874a5ddaf0c3a80bf24a51b68a90fef9107bd20fe94a01157764aab5f300d7e2fcba2178cb80851890a656d89550d0bebf60cca8c23575011d2f37cdc06dcdd8ba010b8fcc764fc8613b1db8d2853b9a0e69a20df78949de07ccc2ad2770f14cc314b5837fd1b5cdb5b52b8fbe0e92aa5d4c8752e24929871c003937a6c43d403ba4e9b0c9c569a0a0c1a7402130d5842ce9eb49f578a3911c899b8e16fb745e9f5ed5e003f91f9b5c56d362fb9850ee370ddc973f6ebdc209fa777438fd65ef9a858f76835e0cc7606c900129245c4901b04dc3a35dee90dc583f9ed5cc9733cd7f167abdcaa87c1c8279e778e55c0cd37ef856025d1fff5ea3c6c3f23485e55046c11b8844f44f171ed7b0784fa586ed9187197fc50b7b9cdafe53c5a4c0199f99e48682a45db1cc4ccb30b65cf462979e3b7f9863aa558796ae3c8088312a7f370133cdb9458d5bcde64a2515c637ffc758ccfc0a8d60144f7dfe68da60db686bd14248e11e5a6bd70261b1396e11498c69d4bf1daab54219e8c1a497f39968303bfc32e2a2b5cd3908e97f82aa57e05ad3715efca4ff25153ffa883d83f10354383de081309315d48fd96fcedb839e7a9a96de4e128ac9938b1ce01c64780387b1d477182b724fa40f0df327d1ab05233a2368ff21ff2035bf2f39f04c9b92b60293575b160f2cc113029dec187a1fed3686c951806efcabbf92cf0fd86809da2cfe7a56546bafe36239d2d51610a1c487d4e7f5f5f5b4ad80f4f31909b151d6cddeac61430792713930ad47d09", - "ring_size": 8, - "prover_idx": 3 - }, - { - "comment": "bandersnatch_sw_sha-512_tai_ring - vector-2", - "sk": "930516bb9a8188741111f0c34f6996913ad705abc614831cb092c1a09dbbf919", - "pk": "7b7433d589da0d7c86ac0be95779aebd39b557c88253c759bb1f11491494bf1480", - "alpha": "0a", - "ad": "", - "h": "5b678275c3b8c968df733aa1d908f655f4fb283f766c579cfb6923312380eb0480", - "gamma": "229a28f9e93569ab142bbdaf4621fb05c148aba2072768a3c2aad7e4bf55550f00", - "beta": "1face71acd5f4f4a97295fee1b0eab3b66f2d27db1d5f4da5628b7666c4b09d9", - "blinding": "3a2cad30984bd4f7f87867a26925361dcd66317bd503298e3abed37774aad108", - "proof_pk_com": "a85601fa16306e57062208f6955390e2a0b046994274786d3c2bea6abde4380400", - "proof_r": "00d5981b5b24ec1bee09563d904304f111bdaf4eeda9144b0aca8ecfe0a7430380", - "proof_ok": "33cd57b070427bdadebd115fb04f2c5dd6b7985eb7fcc2744d744d28772c831100", - "proof_s": "e28ed027aa4f90b503a741b8bb3d92be9aabd38b2688566603a8cb5ff7ba4113", - "proof_sb": "a1b3aca030b593e5c5097566d584ecaeeb9f72a5dc010deae18e66f866b27204", - "ring_pks": "37905ae419e7f44dce9333029263a3bc0a6815eeba2e322fb6030b243ed529138029706abf1e35af7d0b72919c326ef317b74c466a7df6f1c7dbcdf3fa24145c2d80b965a6bde5f9d3b91487c2d40ab9410f6255234b1d5c9d235954ad51e3b9f126807b7433d589da0d7c86ac0be95779aebd39b557c88253c759bb1f11491494bf1480dbfec1c6ad5a4bbc9b40c07b957f9e6fa9381a546fa168a27f43e1c6e120825700a8e8776cd4c9fd5abba95f301c2c641eeba46e4985e810f41d668dbc0ac211410080d3bc3e7feac76b489f015dbc9f6326659ee72d4d270a7ca48e421153f3e750008e2c02c8e15c0279b81e2006197c3b5abe5f68cf59b2fb80dc216c9d60dca55600", - "ring_pks_com": "a2f97a121a14d51dbc14adfec05d5c9d054f5e86d1944f0f3547722ec5076a40269415dea92ab56578903527f38a85e6aec765b2de4a8cbeb33783eddf3e57761d68377a42643ba738b730221646516625bc3883d19e0e581dc660aec1114fa392e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf", - "ring_proof": "8a4a091855e35f3661319ec351cc6f1048bb151bd3cc3ee9fc49d063653a01cbc95b165db39695408652ec4e904657d99107bd20fe94a01157764aab5f300d7e2fcba2178cb80851890a656d89550d0bebf60cca8c23575011d2f37cdc06dcddabb53ef3435264067b5e5252ad348a2c3232a171c4a4c2730b5fac0cb66786251e953e41fd22c7c56b2e9dc69a524d008bb3d5f011a22839518ff2ed7593951b651cc0b65f4cb12903e4dbb27253d893c81fef82bce7bd130b48e2b30b32828f2d28bf27ee8d23d7d22796c76a5afc52cd7e24ca45763cf43e407befa04ccd3ca297ec38df3e6dc751876005622f8d6f5feec3c10dce65e164962170537d814b2b03e309da06a35f15252ea0ef2edc409bb3b89e9c36546164c4dcb21ecd586466465ef3ee7f1efb74e242b8ed19f391e9e4f4e1fd14462e82c766425cefc21457914d7d4b54a443f27d04c4d5228cb40b3bfb59b12ba9bcb8ab4cdb4fd06f1db4b6536bdeef6a1a6b939310921fa9a3c04a0b83565e798b18398181b95c24268f1c130ae42886c5eef862bca4b82a74c90778d026c048a2897cd1fe3ea8e53cb1e529d56062b6412db28c1d3b95f6f22c54b589ff014d8a66a16e46baf1248346f0ec9871cb01545a18f25ced5343e2640fbbb17220fea45cbb556abda7a5f3305b197c1389e7e8169db33c0d85640cb6f29b1b4a0fb4c91e1e5b877ea6fd64d0149b6b20369d52679ad4815428650fb7b81c786e5b456037887d746010b0e8a2e783fa06377e1f5efc61cc211a711bc64436da791364f29d08ccfedb100f9947536d7360348955cf69393e5b11c799", - "ring_size": 8, - "prover_idx": 3 - }, - { - "comment": "bandersnatch_sw_sha-512_tai_ring - vector-3", - "sk": "162578407cef66316bc4f8b37acd8ffdf8270acd9817c5373bd66779a20ecf1b", - "pk": "5f08b092890bed60b015be1e228d515d1bcf83fe254ae8f2c8621b53e33c836a00", - "alpha": "", - "ad": "0b8c", - "h": "aa1a649e5ec9bb921ef7828fc239f80183027bfd07280737edf6f04974c6750200", - "gamma": "4faf6e1431a078c14be293a344d58067b7bdf62999bddeff7c843145e038522f80", - "beta": "445e8b807d95e09c147b0761a46ff84758f145fa467913e57c0b209e7d744abf", - "blinding": "f06965d9912d89aa26b24de126d9d72519308ce4b458f58d0f27d55b59ff8c0e", - "proof_pk_com": "1b3834e85c6c1edb035b73d1c62fa8219a6e6b39fbf7619b69e261f02bc93d5080", - "proof_r": "39c2ba376b065b8fd89fc4d89bdd8b52073f5f4fc35fe8f67ee83d73d1f7a34200", - "proof_ok": "eec00f5462c3acc58cdc4210e770b12830a3a29b390fad3edbeb6ec6af3b373e80", - "proof_s": "4b7bf40c6514edfa8b15dba03bf2f96e4f8b32ea56faf69e9f10e4ff01bcac06", - "proof_sb": "79bbfda713a59183dddbf4744cc67e3f9c3e4aaf8fdb7a34c7a17d818e37cb03", - "ring_pks": "5020519f31ebcc1d5eccb7dcf07cd3c74aeb984ed549e95aaea4c56786c2914b0065213d31169f9ab0da990a8bc1e25f010e253906d260098bf5dccea1580bb25580564a107119782b98508517e38f942d63b70e1818da6d14adf708a1d3ff44294a005f08b092890bed60b015be1e228d515d1bcf83fe254ae8f2c8621b53e33c836a00bb50785c1735184dcce0cdb58183f4463b2ac961243db1c59e979fc8ba4eef2180ffff1066bb536f2fb66f026b2dbb91c7d120a21d02e05a7a7df1326976fb886180b4495d1220c8d89532a8ebdcac21db7a52e54202aaf9d01a0ea35b9b16a6f33b80a2439a22cefaca30ee096ded6ab64185403fcfc311a5741b51a21bcc338d8e4b00", - "ring_pks_com": "97df3b07617bf182d3ffe4d3ce151acf0488bdbb747ad1699a0a5c3fd4bfae12397d7a6f09ec2ae027df39f04e90f074b04a26a126239f176b47928c7b184a9e424e275c06868199d4c59319fcd5c5729d8a4d435db264cf4f17bba845a6161a92e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf", - "ring_proof": "af22e9bf883f46fd330815ae1b735054c4cd1b6b910bef436a66812038de2436198a52f0f8b89e66bcd6c0554d903c389107bd20fe94a01157764aab5f300d7e2fcba2178cb80851890a656d89550d0bebf60cca8c23575011d2f37cdc06dcdd95cd797a54156f5e2ed94d66f8785c0c7c01688337fef8b42d11695904c96dec1024df34b9e04703888101939f85f56b8e7c357f6bb524b91106f8e27db467eba6811ff8d52651c778aa545fa166ee3b9c6fdc5d147503afb3cf432a6fe81db02096c7885acc76b0caad73b81ebcf700b95c6b2f00c3911840597a2ce770274687353b2ea02e3e88b9a47c05e8a289b87883032d6f0eaeefdfecea8b315aa00caa7137578d8e69fad67731b65ffa41b8ba8f0251f51c9d9ee8eb9cd9b330da1cf66de960022d4fa5ef5bfc0705169981d13df9dd128a844efe5865ed5ee3ed3c455e8bee584de3d5365b11fe31c616d915a462b81e22b593485479b13cc48c570e633f7d4930273338ff99a23deb71c13f3f5a8c0279310a91208173d81dba217531c7c846f018aff23c524b7e2100ac1b3f95e79a69772c6c9e5c9dff2917568694170c5297e40f8605f18379edff9aef9f431c560a9874715905ea8c1edc9ab9d3db5006c92817fd62e31fb6f929e7ab044938a87835a1ed1ade55357a7a999bb594ba61c239f9aee76bfa114e8a46a055d020c20e813accff1f0bbf4b494c47eed850aa5b451cb6f0afb927add35e40cc8c5c5cb862df73cc21046b0638048b4ddd956b34306238627c932715ea301b084257cc00aad725f4860568d37f21586930c9dc9c09e9701813e5afbec2b8", - "ring_size": 8, - "prover_idx": 3 - }, - { - "comment": "bandersnatch_sw_sha-512_tai_ring - vector-4", - "sk": "17a1ff6e2f640390c0daf68df221212b79f5d5b8c88a610b4ce26489af066b01", - "pk": "5a6d13e3cd5c95285ac88c1e09125426469ed180b6d5e7d63c1b1f49f8cb877000", - "alpha": "73616d706c65", - "ad": "", - "h": "f470cdec5856dc89de0714ad58c34fdaf665a9be69f40e3f3ea11f5df7f3343380", - "gamma": "aefd62fa4370d591105083e066f18ea9c1385b2108ebd8218052f821686fe14480", - "beta": "8cd4da8facab33febd81a501bfd944db3afdec1ba4c5e7015a79851a834b3a4a", - "blinding": "d197c4d1733d7fd97afb6d8e9d18d61b470e2818a52124aee01e8196a3770615", - "proof_pk_com": "fd70ec5877d0446e5cc9364dad6c2d2d4172b039226ccc74f98a208b8cc78d2800", - "proof_r": "ac535a8c53c7e33fd72116b9c64cce71158e6f9cf15bfe9c574c4a572a04ff4480", - "proof_ok": "9c1555853acb686809d7827afebf6de9c5c98312436410afc272ebb868d8aa5600", - "proof_s": "41a987008d013ebd10030ca5994d85cd8bd957f2940c9c3d015342315b3d4700", - "proof_sb": "573aeaea8072de85eef603a0c44c7f07ec212086cbb91b672e2456fe4a218809", - "ring_pks": "d107238cda2a7b7cf82f192332b7ca9f3e6d312a7b64c5c2f9d37f2e921ecb2e801488cb2f702595906361876a32ef069decb3e05bc2556d3e6366c65470ea9c2f007cb730f1a12a4e73d8923201098902d6a07d2f79e3e13cc347dcf821de6c7260805a6d13e3cd5c95285ac88c1e09125426469ed180b6d5e7d63c1b1f49f8cb87700059f34600ce0dcf0f085948d43ba7878fa9b6004c847ad6f9e12681d0b1aefa3a0011328ce6b64179b29e88d5007673c8cbf3e676cfd82492f73dea845aa7dfd86700d0521a11107fc24adce3c0562d01175c650ee091e494ea94e6df25a70213bd3c00b489f39114c2be2d3890a887415377dad3c3fbc6c9e3f4e9d19d77585053950f00", - "ring_pks_com": "97a3298066dc1b2d9b17403a4abbc93c33120e3c67af79ae615de1a1dfb327e6da25da8c6583b6d2cf4d8855403c61b6932ff852225ffadfd8fb0857e5011118736ac3c3e2d1c8cd0aeeff564b4d73f3c3f44d1563fe387e1e548ab62838ec1992e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf", - "ring_proof": "8e9177efdb113ef2d96a56fb65e57ddf8a520150b227a649f0b7f5bf754b0c9f63709f8f6f3b7c35a9f658c2aaa6350d9107bd20fe94a01157764aab5f300d7e2fcba2178cb80851890a656d89550d0bebf60cca8c23575011d2f37cdc06dcddb5efc1fc797b375945197f9e2e75afbcfa3457611f8da81900461332000fd7dde8ddf42b9bc493256926e390b2f2a8aaa9ed78d7c9ac6a5d8241b92a9f34b84fc1e315806ca43cb95066125837b89eadb18e695405442848075cd717f742e0561d0bce409d51ad3b3d15f35e789a5b6598459e6e7d5589d4adb1966a5a271104dc112a1afd62e4dec6a5e44a98db0a461d3dadfef5b812d1ad17ccb506e51a732db8685141881c40ae3819a72385b178d824c5ff9d09f2e223282c95abd6cb063de44e73367f2e65948a933b88b53e381237895f2212daeb3d718eabe6469e50f8fa7e949e691cfc76c695e3b595df1267fbff4f1058debd5c92172231cafb15b9c807687212cd6d110df0580b4f63ac2aec3acedfc35af696227b204d690e53c2384c4a2cd44ebff3091dd78bdc671fff61e45c249b8d94a85e0a9f29a49c0b8893d2e722c0de2c07768d506702872905e78cac57b8caad891f8ddee1417ee3f003533ec331bddb5d11954c4429272d339a295b45f3543b49f36d0f882c21201b0009c0b2cc00c123e8ea328ad0c0149717bf68d55f3c2c3bd89303ca49a0e04eecc48b3c4514c59794242ef0cd441725f2fac338d1595a1ad49e8e4c261c538c1de5a21b6f021984c4080f97c5e4aa34ed576b6075764d3b38e1aa676059a077101416dc1d6b23c60e8a114a81c0a2", - "ring_size": 8, - "prover_idx": 3 - }, - { - "comment": "bandersnatch_sw_sha-512_tai_ring - vector-5", - "sk": "fd47a91def04dc1fc7492337ea29740bf5cad047be5fcddd60976e55ab062817", - "pk": "cfe9be2fa584b7387a76d4537f81ec1b8a1210a9aac7ae72c87bdfcb4c0c761700", - "alpha": "42616e646572736e6174636820766563746f72", - "ad": "", - "h": "673218a4b0b407e8f5c02f3593a80c7bf226fd599e023a5135fe056b29eec01a00", - "gamma": "570b39f4c9a058ed6c652e4d94a6020e7afe7b14c58c1e67ce28b5894bd9c24080", - "beta": "c7261d5a43ed02273093e89157198efc3d1d3906038bb43ffbb47d1848833545", - "blinding": "0cf83dce52d08b7798eb9144d912b7b31204fe92896af3e792236be115f76919", - "proof_pk_com": "ac3b3a47430e1a5cbe997df2951e5fe13fed96d613522b9ba385b256191ada5400", - "proof_r": "e53915bcfc3aaf3ffde07b0e9d7fbf7d1d7f34bffb8ab82cb827028c596cc70700", - "proof_ok": "6876624d12db6c30c864fdbacb718ec0e6385108b11539f16bf2d3456316d94500", - "proof_s": "a1b971d14062080f7a5ead1ef8a19877823447faa9fdf34141fd9eac6c10531c", - "proof_sb": "53fd9abae2688ddd003f40a79229b4a24790dd02245e0111e676b39ea5424713", - "ring_pks": "19883e70df937bfcfba2d32906da291e47d5d05e436388ae0ae42ba16ae7450880d61d2cb5fce6d53b430cd810f59cddd491e031a9a30a89bcb60b2793a97a77710057378019a19e33eb8259997af049555932e9be2861670e2b0ac328b91961bd6900cfe9be2fa584b7387a76d4537f81ec1b8a1210a9aac7ae72c87bdfcb4c0c76170011e948c31d8eb6b01778bff5941a70ea086f7f48a30e08ecc6e94e2b8c942d1e00dbb25acd5139a72f8ac173905670744d97768ba710c6c31f0bcc11edb051723c8075a78410bccb50de3b12f64ccb7bcbd5d5842089adc665d6024e4030d1d9af0f80b531f36d1604d5363dae87ecf6152cc357aaa23ebfa2c9540de345e85e71645e00", - "ring_pks_com": "ac28272b59ace95a3673b382952de47c65afc50e130ede0fbde9623ead9e8c7e6b5b6feb4272b91a94a3fe90e2d12e568b404131dbb727349e84fb1c8f9a6ed4f259256ae712f4774e611247b65f3d2dbf77aa701840fc6867f8edc596d0bdcc92e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf", - "ring_proof": "98d740fbf01f88496783222e510cf94145bd8f8b9aea5fdf2b363add9d0fb6ad7a68ab6abe59a91a6f69708783f671389107bd20fe94a01157764aab5f300d7e2fcba2178cb80851890a656d89550d0bebf60cca8c23575011d2f37cdc06dcdda80e66b0614769e66d0693aeae1f81e98bb96a540ece93675e3dce9d5884fd1ee76bccd0402ea6c7d0fd8684fe4f53bbb3e323d5ab16d695eb141ce885cecc2dc595c55422dac72fd4ee68f0042df087696e4b7578096cb22e4cbca6dcb22eb131244b62c4a79d6135045171de1480fe3df3af85c333006a378cbdb0e599774f1df460510bb4ff82698643c53fc5a28a2d9573349f5ba24d49f36d9a84e9a022df132d97d3a42ad7c2a9ad5ac5bdfa66f30ba59dad2dbb7c633df4f3d1b61572db43a23e0cd9196fbc3160c9d577706e7b8757f00f8379f5940a7a6c6bd7fc52f8851371880437a9e86ee571d983675c1b530ece1f44e0aed54ad5ddf15419089b2c83557632968970b15ee4ecf56d3bee68f4f7bcfef2ab65531a1bd5e7cd51d2fcd8aeae8426af285690a2ba5b8e82fcbc0012e6742b531b9c5cee24b45873b68500de721c74f7be9e4ce407a2ae0bc3e3185b1bbd3266710a4603d8dca9bdd2eb2b6bf3c8a4019f100aba63a5595552046c7ec7dda27dd8d24bb7b33811a74729a928d163f051f2992d9b5d962c248ee53bc60e89d79fae62e8c0f69a7d3d9d83a812091a584fb8c1d19a2185ebdcb3ae9c85552a4a42c33f44014ed9386fa4aa2831a23ab260a15819ad91701ea13ee4c275bb60564a7c2f2f774c8914134cf5b0f492d8edd48300f42f31fd9a46", - "ring_size": 8, - "prover_idx": 3 - }, - { - "comment": "bandersnatch_sw_sha-512_tai_ring - vector-6", - "sk": "fd47a91def04dc1fc7492337ea29740bf5cad047be5fcddd60976e55ab062817", - "pk": "cfe9be2fa584b7387a76d4537f81ec1b8a1210a9aac7ae72c87bdfcb4c0c761700", - "alpha": "42616e646572736e6174636820766563746f72", - "ad": "1f42", - "h": "673218a4b0b407e8f5c02f3593a80c7bf226fd599e023a5135fe056b29eec01a00", - "gamma": "570b39f4c9a058ed6c652e4d94a6020e7afe7b14c58c1e67ce28b5894bd9c24080", - "beta": "c7261d5a43ed02273093e89157198efc3d1d3906038bb43ffbb47d1848833545", - "blinding": "20537ef560d5a8d7483dec55e0b680797376e61d0844bf6971135cd5811ee203", - "proof_pk_com": "1db1d10f797d6208fa724359100b6297c74a98a3f35626ad8dedf891d362ac5780", - "proof_r": "2d6643ffbdc2d4b11024be73349dda34fb1922c7d67a617471bf05ce93f29c1e00", - "proof_ok": "db649772438941f07e1cb4d92ab387aaae22fcb0389d32668a54385a0bd8461580", - "proof_s": "505e46a525c967e840254400b59e75fb40ea21d3c132bcc5032df23bf3dde514", - "proof_sb": "f71544be7ce3ce1a4473d30070c427f84be2f80c080a008c1a4280e40754ff03", - "ring_pks": "19883e70df937bfcfba2d32906da291e47d5d05e436388ae0ae42ba16ae7450880d61d2cb5fce6d53b430cd810f59cddd491e031a9a30a89bcb60b2793a97a77710057378019a19e33eb8259997af049555932e9be2861670e2b0ac328b91961bd6900cfe9be2fa584b7387a76d4537f81ec1b8a1210a9aac7ae72c87bdfcb4c0c76170011e948c31d8eb6b01778bff5941a70ea086f7f48a30e08ecc6e94e2b8c942d1e00dbb25acd5139a72f8ac173905670744d97768ba710c6c31f0bcc11edb051723c8075a78410bccb50de3b12f64ccb7bcbd5d5842089adc665d6024e4030d1d9af0f80b531f36d1604d5363dae87ecf6152cc357aaa23ebfa2c9540de345e85e71645e00", - "ring_pks_com": "ac28272b59ace95a3673b382952de47c65afc50e130ede0fbde9623ead9e8c7e6b5b6feb4272b91a94a3fe90e2d12e568b404131dbb727349e84fb1c8f9a6ed4f259256ae712f4774e611247b65f3d2dbf77aa701840fc6867f8edc596d0bdcc92e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf", - "ring_proof": "ae666c81112ae53e1f1c99a407ec99101fc065e1ec45bafc7e1cc158fdc0ae20fbefdeb7d9ed20753f65413eff532fa79107bd20fe94a01157764aab5f300d7e2fcba2178cb80851890a656d89550d0bebf60cca8c23575011d2f37cdc06dcdda8b466de672aae10748e9da7e61ebf22c3bf1bb99f4deb5918aa57b7c302e82a9d2604cddf28eba6f8bdecb5b56c92f7870f54b5dc6d48a661c04fd7b22ade4370b146d9f9190869487064f2c45e55c3c743d85af87d44c6a325d080116ef6d9fbca731b51dcb8953486512ef7ae45d7315b7fcccdb8f044360a79facf157e07468e6893d801a996027d43b88597efef4fc1b97d3f08669a951da599649c0f6486059b6b1995de67335b7c21afa78bec05207c55c6c6716e27ac8ae54238f038bdfe8b917808f0379d099b979441a0b82eb738286e2293e4cc538d3c3c57327054b7f45ead6fb00848834a5061482c90663eaa2f9d386d9154a62573bb9cd80de295b6e3dc8cd775e4fa4bbdc92021cb318d9228d88f361784f78530bae3b54d9c04041522b310d4fbfb4021162424c7909a2acac7c58711231d69bd56ac0728a52061e54318dbd25b32b95b478dbf3331bc501a104b47dcf33b117a0270df9ab559adf01fa04fe8df0ff5d4f14f765d7040daf3a67d6eb707e34af8b28f06acb3143dc353a9a4c908f84ab536a96b26b4ff603449642ed8d952f16981aa3e4d83133f67f4b4453af32554e3e047f0945eb4d940693b9e69da0525d1f344ec3d825b77a3443dd23ae3c531722edd680cd80871752d3f04d31975b6d45218d0e5a528a2721e8a404cf30a5c196a92da5f", - "ring_size": 8, - "prover_idx": 3 - }, - { - "comment": "bandersnatch_sw_sha-512_tai_ring - vector-7", - "sk": "f00b6b8332ed0d7d1ca8aaf3a6afcd146b475d43880ff97ebe088ed7d6340601", - "pk": "e690d4b47a99a0434b40217f2228f029a49485874cff87066a822637de41360b00", - "alpha": "42616e646572736e6174636820766563746f72", - "ad": "1f42", - "h": "673218a4b0b407e8f5c02f3593a80c7bf226fd599e023a5135fe056b29eec01a00", - "gamma": "1db8258d742dcbcc1334322338c68d51cfe24301bdc29cd79f8d01c133cd8f1680", - "beta": "99ac81017e84d4cbc8f0b79dcfe3cc7f8899f8e1b8cf71a5194551dd79771d4b", - "blinding": "2cff0f18f6fa445491223aeff1e117c6180a555aa23dd710f1d785c24abf870a", - "proof_pk_com": "caa63e87aed8ca3880a2d63d97dfb9f47c8add13dbf5fc2c80cf410d7c66ed1500", - "proof_r": "ac7737523563fc44092829f1d20a02c50e02380cefc6c871fd32337bf62da15c80", - "proof_ok": "e8a1911928b58371fe29722b83ac3721bb74e85a5a85f549edcaac082293373780", - "proof_s": "0a228726dd7e3350d84d7b6d6e5f8a0b91cf6860d91157f1612df05a2a8f3c0c", - "proof_sb": "87e7717f25017e1855e30decb2bf83029e8a56fa166872ee9e408a581cb61b0a", - "ring_pks": "730a10d81ce68c343ce661fbc2e967e7f0ee7214757542fc294da5e17769ce2300ba33958c71aaea5cba62057d243886ca5f00ca2ef1707605627d965c9e0bf84f804fc605c3ef299b01d76f5561684ae95ee6972bb435b15a6c26ed0917fc50c93480e690d4b47a99a0434b40217f2228f029a49485874cff87066a822637de41360b00b04e353b452753712f3d7ce66683541ebeccbf5bf346f815e40372474a1f3169805213841b553883e232db641b3e836d53d5a5d7954e9b232531c23c1253b1bb258072abe896313634f300a0c5232388dc6310d8969c0fc958386a5fd01ef617261f003ec2cb56692e8b855713125ed6a2b53943ad380ff04af107448dd9bc6cfaaf3a80", - "ring_pks_com": "92bbe9677511a1d04ca8f300f000c5e4bf9eecbac6e0af709bfa6f9fad456f6141f8f301ea69ffd72ed51d56cc9394f182856fef4061770e9e14b33d9ce7d8a7d6bcdbe507df9ebcc7c49d016441f01df5fce431cfab70659cb951c6490cecc092e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf", - "ring_proof": "a199113fa3b5e6bf315f600fe31fd8b9f8aff50d9dbd878e277b3d8f443689fdbebc216e7404e8eecb4b6b2e0487bc219107bd20fe94a01157764aab5f300d7e2fcba2178cb80851890a656d89550d0bebf60cca8c23575011d2f37cdc06dcdd90490cfb5cf225366d8bbaa6520149d47dd3f036a1ebfe01c6d379c96055f4fe23e2b615973a24b7002d57885726b8aba2925f7ec404a7c3321e7cd5e9f1670f8fd3a055f314796ae34146d4db5180f3c9caa95d926203194ac81c6642a81c49d155b64921286de0a2e1c945ac53ab4dd601a3a04636d18981954da8dfd0d570358b2e5704b7f785d413c89d9f32a76c9129d4781367f6765f97861a4b36e72322dae30e78fc3e96508f5bf8ea34b0e059dc12fa65944bdff8e9f38604a8d011bd586ab17a2ea5dc4e3135d951b6bccc757e9bf4f7333c2b4f2dd69653337e00ca0ff7e0a548772e9023352b00bb7b8a18b9bc371c6e70aa58a15bec34d5d94982798be042fa9927df38e024c1e2782b654c7e978614594433bc9e7259febd0ef14182e306615d7a3c8ccdfae07efd5aa27eecfbbb274e663d7a9e09dc7bb222b3e2e22d18c7b6d5b4d0f7028b9af777443852b7610df60a3f719bcd176519cbe97e142aa1f3d223e930354fccb2529d03d4ef86598f79a8ee7fcacc2a58788957d963e77b2a991d99f3381fd98f2739a01b5b6c1989d597a6c2255c5a15dd0b96f4239910ad2fc023cec38ee28140ba4e924a86ab3304ca211b994391d08bcea124940df103dbd663019722d87470d8e4828e2014ea190f5b57e62efd836c704eacae53af187cdf878d1a42b3ce5d0f", - "ring_size": 8, - "prover_idx": 3 - } -] \ No newline at end of file From a99126b3f7b376738d2cc9f063dbef9231426f22 Mon Sep 17 00:00:00 2001 From: Prasad Kumkar Date: Fri, 19 Jun 2026 22:46:29 +0530 Subject: [PATCH 2/4] Clean up curve and ring proof APIs --- dot_ring/ring_proof/columns/columns.py | 28 ++-- dot_ring/ring_proof/constants.py | 89 ------------ dot_ring/ring_proof/params.py | 65 ++++----- dot_ring/ring_proof/polynomial/fft.py | 44 +----- dot_ring/ring_proof/verify.py | 129 ++++++++---------- dot_ring/vrf/delinearize.py | 28 ---- dot_ring/vrf/ietf/thin.py | 56 ++++++-- dot_ring/vrf/ietf/tiny.py | 33 ++--- dot_ring/vrf/pedersen/batch_verifier.py | 43 +++++- dot_ring/vrf/pedersen/vrf.py | 86 +++++++++--- dot_ring/vrf/ring/batch_verifier.py | 33 ++++- dot_ring/vrf/ring/members.py | 14 +- dot_ring/vrf/ring/proof_builder.py | 12 ++ dot_ring/vrf/ring/proof_payload.py | 56 +++----- dot_ring/vrf/ring/vrf.py | 100 ++++---------- dot_ring/vrf/transcript.py | 46 +++++-- dot_ring/vrf/vrf.py | 37 +++-- tests/test_ark_vrf.py | 2 - tests/test_coverage/test_columns.py | 4 +- tests/test_coverage/test_fft.py | 29 ++-- tests/test_coverage/test_ops.py | 41 +++--- tests/test_coverage/test_params.py | 74 ++-------- tests/test_coverage/test_pcs_utils.py | 6 +- tests/test_curve_ops/test_gaps.py | 45 ------ tests/test_ring_vrf/test_audit_regressions.py | 10 +- tests/test_verify_ring_sig.py | 62 +++++---- 26 files changed, 516 insertions(+), 656 deletions(-) delete mode 100644 dot_ring/ring_proof/constants.py delete mode 100644 dot_ring/vrf/delinearize.py diff --git a/dot_ring/ring_proof/columns/columns.py b/dot_ring/ring_proof/columns/columns.py index dd877cb..aaeafa0 100644 --- a/dot_ring/ring_proof/columns/columns.py +++ b/dot_ring/ring_proof/columns/columns.py @@ -5,8 +5,12 @@ from dataclasses import dataclass, field from dot_ring.curve.point import CurvePoint -from dot_ring.ring_proof.constants import DEFAULT_SIZE, MAX_RING_SIZE, OMEGAS, S_PRIME, ZK_ROWS -from dot_ring.ring_proof.params import RingProofParams +from dot_ring.ring_proof.params import ( + DEFAULT_DOMAIN_SIZE, + DEFAULT_MAX_RING_SIZE, + ZK_ROWS, + RingProofParams, +) from dot_ring.ring_proof.pcs.kzg import KZG from dot_ring.ring_proof.pcs.protocol import PCS, G1Commitment from dot_ring.ring_proof.polynomial.fft import inverse_fft @@ -20,12 +24,12 @@ class Column: evals: list[int] coeffs: list[int] | None = None commitment: G1Commitment | None = None - size: int = DEFAULT_SIZE + size: int = DEFAULT_DOMAIN_SIZE def interpolate( self, - domain_omega: int = OMEGAS[DEFAULT_SIZE], - prime: int = S_PRIME, + domain_omega: int, + prime: int, hidden: bool = False, test_vectors: bool = False, ) -> None: @@ -68,13 +72,19 @@ class WitnessColumnBuilder: producer_index: int secret_t: int params: RingProofParams = field(default_factory=RingProofParams, repr=False) - size: int = DEFAULT_SIZE - omega: int = OMEGAS[DEFAULT_SIZE] - prime: int = S_PRIME - max_ring_size: int = MAX_RING_SIZE + size: int = DEFAULT_DOMAIN_SIZE + omega: int | None = None + prime: int | None = None + max_ring_size: int = DEFAULT_MAX_RING_SIZE padding_rows: int = 4 test_vectors: bool = False + def __post_init__(self) -> None: + if self.omega is None: + self.omega = self.params.omega + if self.prime is None: + self.prime = self.params.prime + @classmethod def from_params( cls, diff --git a/dot_ring/ring_proof/constants.py b/dot_ring/ring_proof/constants.py deleted file mode 100644 index 966d9d0..0000000 --- a/dot_ring/ring_proof/constants.py +++ /dev/null @@ -1,89 +0,0 @@ -from __future__ import annotations - -from dot_ring.curve.specs.bandersnatch import BANDERSNATCH_PARAMS - -_AUXILIARY_POINTS = BANDERSNATCH_PARAMS.auxiliary_points -if _AUXILIARY_POINTS.blinding_base is None or _AUXILIARY_POINTS.accumulator_base is None or _AUXILIARY_POINTS.padding_point is None: - raise ValueError("Bandersnatch ring-proof auxiliary points are not configured") - -S_PRIME: int = BANDERSNATCH_PARAMS.field_modulus # Finite‑field modulus p -S_ORDER: int = BANDERSNATCH_PARAMS.subgroup_order # Prime subgroup order r - - -# Base point for the prover’s blinding factor when masking linkability tags -Blinding_Base: tuple[int, int] = _AUXILIARY_POINTS.blinding_base - -# Initial seed generator for hashing ring items → curve points (old) -# SeedPoint: tuple[int, int] = ( -# 3955725774225903122339172568337849452553276548604445833196164961773358506589, -# 29870564530691725960104983716673293929719207405660860235233811770612192692323, -# ) -# - -# new seed point -SeedPoint: tuple[int, int] = ( - _AUXILIARY_POINTS.accumulator_base[0], - _AUXILIARY_POINTS.accumulator_base[1], -) - - -# old -# # Padding generator so we can commit to short rings without leaking |R| -# PaddingPoint: tuple[int, int] = (23942223917106120326220291257397678561637131227432899006603244452561725937075, -# 1605027200774560580022502723165578671697794116420567297367317898913080293877) - -# new padding point -PaddingPoint: tuple[int, int] = ( - _AUXILIARY_POINTS.padding_point[0], - _AUXILIARY_POINTS.padding_point[1], -) - - -S_A: int = 10773120815616481058602537765553212789256758185246796157495669123169359657269 -S_B: int = 29569587568322301171008055308580903175558631321415017492731745847794083609535 - -DEFAULT_SIZE: int = 512 - -OMEGA_2048: int = 49307615728544765012166121802278658070711169839041683575071795236746050763237 -OMEGA_1024 = pow(OMEGA_2048, 2048 // 1024, S_PRIME) -OMEGA_512: int = pow(OMEGA_2048, 2048 // 512, S_PRIME) - -# Pre‑compute the entire evaluation domain for fast access. -D_512: list[int] = [pow(OMEGA_512, i, S_PRIME) for i in range(512)] -D_1024: list[int] = [pow(OMEGA_1024, i, S_PRIME) for i in range(1024)] -D_2048: list[int] = [pow(OMEGA_2048, i, S_PRIME) for i in range(2048)] - -OMEGAS = { - 512: OMEGA_512, - 1024: OMEGA_1024, - 2048: OMEGA_2048, -} - -EVAL_DOMAINS = { - 512: D_512, - 1024: D_1024, - 2048: D_2048, -} - -MAX_RING_SIZE: int = 255 # Upper bound enforced by the constraint system - -ZK_ROWS: int = 3 # Number of random blinding rows for zero-knowledge (matches Rust ZK_ROWS) - - -__all__ = [ - "S_PRIME", - "S_ORDER", - "SeedPoint", - "PaddingPoint", - "Blinding_Base", - "S_A", - "S_B", - "OMEGA_2048", - "OMEGA_1024", - "OMEGA_512", - "DEFAULT_SIZE", - "D_512", - "D_2048", - "MAX_RING_SIZE", - "ZK_ROWS", -] diff --git a/dot_ring/ring_proof/params.py b/dot_ring/ring_proof/params.py index 2e40026..2cc94a1 100644 --- a/dot_ring/ring_proof/params.py +++ b/dot_ring/ring_proof/params.py @@ -6,19 +6,14 @@ from dot_ring.curve.curve import CurveVariant from dot_ring.curve.specs.bandersnatch import Bandersnatch from dot_ring.curve.twisted_edwards.te_curve import TECurve -from dot_ring.ring_proof.constants import ( - D_2048, - DEFAULT_SIZE, - EVAL_DOMAINS, - MAX_RING_SIZE, - OMEGA_2048, - OMEGAS, - S_PRIME, - ZK_ROWS, -) from dot_ring.ring_proof.pcs.kzg import KZG from dot_ring.ring_proof.pcs.protocol import PCS +ROOT_OF_UNITY_2048 = 49307615728544765012166121802278658070711169839041683575071795236746050763237 +DEFAULT_DOMAIN_SIZE = 512 +DEFAULT_MAX_RING_SIZE = 255 +ZK_ROWS = 3 + # 2047-member rings need a 4096-row PIOP domain and a 16384-point radix # domain. KZG grows its SRS lazily when these larger quotient polynomials are # requested. @@ -38,27 +33,24 @@ def _next_power_of_two(n: int) -> int: return 1 << (n.bit_length()) -def _omega_for_domain(domain_size: int, prime: int = S_PRIME, base_root: int = OMEGA_2048, base_size: int = 2048) -> int: +def _omega_for_domain( + domain_size: int, + prime: int, + base_root: int, + base_size: int = 2048, +) -> int: if base_size % domain_size != 0: raise ValueError(f"Domain size {domain_size} must divide {base_size}") - if _uses_precomputed_bls_domain(prime, base_root, base_size) and domain_size in OMEGAS: - return OMEGAS[domain_size] return pow(base_root, base_size // domain_size, prime) -def _uses_precomputed_bls_domain(prime: int, base_root: int, base_size: int) -> bool: - return prime == S_PRIME and base_root == OMEGA_2048 and base_size == 2048 - - @lru_cache(maxsize=32) def _domain_for_size( domain_size: int, - prime: int = S_PRIME, - base_root: int = OMEGA_2048, + prime: int, + base_root: int, base_size: int = 2048, ) -> tuple[int, ...]: - if _uses_precomputed_bls_domain(prime, base_root, base_size) and domain_size in EVAL_DOMAINS: - return tuple(EVAL_DOMAINS[domain_size]) omega = _omega_for_domain(domain_size, prime, base_root, base_size) domain = [1] * domain_size current = 1 @@ -125,17 +117,20 @@ def _extend_root_to_size(base_root: int, base_size: int, target_size: int, prime @dataclass class RingProofParams: - domain_size: int = DEFAULT_SIZE - max_ring_size: int = MAX_RING_SIZE + domain_size: int = DEFAULT_DOMAIN_SIZE + max_ring_size: int = DEFAULT_MAX_RING_SIZE padding_rows: int = 4 radix_domain_size: int | None = None - prime: int = S_PRIME - base_root: int = OMEGA_2048 + base_root: int = ROOT_OF_UNITY_2048 base_root_size: int = 2048 pcs: type[PCS] = field(default=KZG, compare=False, hash=False, repr=False) test_vectors: bool = False cv: CurveVariant[int] = field(default_factory=lambda: Bandersnatch, compare=False, hash=False) + @property + def prime(self) -> int: + return self.cv.curve.params.field_modulus + @property def scalar_bits(self) -> int: return self.cv.curve.params.subgroup_order.bit_length() @@ -154,8 +149,6 @@ def __post_init__(self) -> None: def _validate_curve(self) -> None: if not isinstance(self.cv.curve, TECurve): raise ValueError(f"{self.cv.name} ring proofs require a Twisted Edwards curve") - if self.cv.curve.params.field_modulus != self.prime: - raise ValueError(f"{self.cv.name} ring proofs require field modulus {self.prime}") auxiliary_points = self.cv.curve.params.auxiliary_points for name in ("blinding_base", "accumulator_base", "padding_point"): @@ -180,6 +173,10 @@ def _validate_domain_sizes(self, radix_domain_size: int) -> None: raise ValueError(f"domain_size {self.domain_size} exceeds supported SRS domain size {MAX_PIOP_DOMAIN_SIZE}") def _configure_base_root(self, radix_domain_size: int) -> None: + if self.base_root_size % radix_domain_size != 0 and radix_domain_size <= self.base_root_size: + raise ValueError(f"radix_domain_size {radix_domain_size} must divide base_root_size {self.base_root_size}") + if pow(self.base_root, self.base_root_size, self.prime) != 1 or pow(self.base_root, self.base_root_size // 2, self.prime) == 1: + raise ValueError(f"{self.cv.name} ring proofs require a primitive {self.base_root_size}-th root of unity") if radix_domain_size > self.base_root_size: root, size = _extend_root_to_size(self.base_root, self.base_root_size, radix_domain_size, self.prime) self.base_root = root @@ -200,7 +197,7 @@ def _validate_ring_capacity(self) -> None: "domain_size is too small for the scalar bit decomposition: " f"domain_size={self.domain_size}, scalar_bits={self.scalar_bits}, padding_rows={self.padding_rows}" ) - if self.max_ring_size == MAX_RING_SIZE and max_supported != MAX_RING_SIZE: + if self.max_ring_size == DEFAULT_MAX_RING_SIZE and max_supported != DEFAULT_MAX_RING_SIZE: self.max_ring_size = max_supported elif self.max_ring_size > max_supported: raise ValueError(f"max_ring_size {self.max_ring_size} exceeds supported size {max_supported}") @@ -219,10 +216,7 @@ def radix_omega(self) -> int: @property def radix_domain(self) -> list[int]: - radix_domain_size = self._radix_domain_size - if radix_domain_size == 2048 and self.base_root_size == 2048 and self.base_root == OMEGA_2048: - return list(D_2048) - return list(_domain_for_size(radix_domain_size, self.prime, self.base_root, self.base_root_size)) + return list(_domain_for_size(self._radix_domain_size, self.prime, self.base_root, self.base_root_size)) @property def radix_shift(self) -> int: @@ -251,8 +245,7 @@ def from_ring_size( cls, ring_size: int, padding_rows: int = 4, - prime: int = S_PRIME, - base_root: int = OMEGA_2048, + base_root: int = ROOT_OF_UNITY_2048, base_root_size: int = 2048, test_vectors: bool = False, cv: CurveVariant[int] = Bandersnatch, @@ -268,8 +261,7 @@ def from_ring_size( Args: ring_size: Number of members in the ring padding_rows: Number of padding rows (default: 4) - prime: Field prime (default: S_PRIME) - base_root: Base root of unity (default: OMEGA_2048) + base_root: Base root of unity (default: ROOT_OF_UNITY_2048) base_root_size: Base root size (default: 2048) Returns: @@ -288,7 +280,6 @@ def from_ring_size( domain_size=domain_size, max_ring_size=max_ring_size, padding_rows=padding_rows, - prime=prime, base_root=base_root, base_root_size=base_root_size, test_vectors=test_vectors, diff --git a/dot_ring/ring_proof/polynomial/fft.py b/dot_ring/ring_proof/polynomial/fft.py index db85b58..3d0c6d5 100644 --- a/dot_ring/ring_proof/polynomial/fft.py +++ b/dot_ring/ring_proof/polynomial/fft.py @@ -7,7 +7,7 @@ from functools import lru_cache -from dot_ring.ring_proof.constants import S_PRIME +from dot_ring.curve.specs.bandersnatch import BANDERSNATCH_FIELD_MODULUS from dot_ring.ring_proof.polynomial.ntt import BlsScalarNTTPlan @@ -56,15 +56,6 @@ def _get_twiddle_factors(n: int, omega: int, prime: int) -> list[list[int]]: return twiddles -def _use_native_ntt(prime: int) -> bool: - return prime == S_PRIME - - -@lru_cache(maxsize=1024) -def _get_native_ntt_plan(n: int, omega: int, prime: int): - if not _use_native_ntt(prime): - raise ValueError("native NTT is only available for the BLS12-381 scalar field") - return BlsScalarNTTPlan(_get_twiddle_factors(n, omega, prime), _get_bit_reverse(n)) def _fft_in_place(coeffs: list[int], omega: int, prime: int) -> None: @@ -78,29 +69,8 @@ def _fft_in_place(coeffs: list[int], omega: int, prime: int) -> None: n = len(coeffs) if n == 1: return - - if not _use_native_ntt(prime): - rev = _get_bit_reverse(n) - for i, j in enumerate(rev): - if i < j: - coeffs[i], coeffs[j] = coeffs[j], coeffs[i] - - m = 2 - while m <= n: - half_m = m >> 1 - w_step = pow(omega, n // m, prime) - for start in range(0, n, m): - w = 1 - for j in range(half_m): - u = coeffs[start + j] - v = coeffs[start + j + half_m] * w % prime - coeffs[start + j] = (u + v) % prime - coeffs[start + j + half_m] = (u - v) % prime - w = w * w_step % prime - m <<= 1 - return - - _get_native_ntt_plan(n, omega, prime).transform(coeffs) + + BlsScalarNTTPlan(_get_twiddle_factors(n, omega, prime), _get_bit_reverse(n)).transform(coeffs) def _fft_in_place_scaled(coeffs: list[int], omega: int, prime: int, scale: int) -> None: @@ -114,13 +84,7 @@ def _fft_in_place_scaled(coeffs: list[int], omega: int, prime: int, scale: int) coeffs[0] = (coeffs[0] * scale) % prime return - if not _use_native_ntt(prime): - _fft_in_place(coeffs, omega, prime) - for i, value in enumerate(coeffs): - coeffs[i] = (value * scale) % prime - return - - _get_native_ntt_plan(n, omega, prime).transform_scaled(coeffs, scale) + BlsScalarNTTPlan(_get_twiddle_factors(n, omega, prime), _get_bit_reverse(n)).transform_scaled(coeffs, scale) def inverse_fft(values: list[int], omega: int, prime: int) -> list[int]: diff --git a/dot_ring/ring_proof/verify.py b/dot_ring/ring_proof/verify.py index 11137cc..c7bbdc8 100644 --- a/dot_ring/ring_proof/verify.py +++ b/dot_ring/ring_proof/verify.py @@ -1,7 +1,7 @@ from typing import Any, NamedTuple from dot_ring.curve.point import CurvePoint -from dot_ring.ring_proof.constants import OMEGA_2048, S_PRIME +from dot_ring.ring_proof.params import ROOT_OF_UNITY_2048 from dot_ring.ring_proof.pcs.kzg import KZG from dot_ring.ring_proof.pcs.protocol import PCS from dot_ring.ring_proof.pcs.utils import ( @@ -48,12 +48,6 @@ def witness_commitments(self) -> tuple[Any, Any, Any, Any]: return (self.c_b, self.c_accip, self.c_accx, self.c_accy) -def _ring_proof_fields(proof: RingProofFields | tuple[Any, ...]) -> RingProofFields: - if isinstance(proof, RingProofFields): - return proof - return RingProofFields(*proof) - - def _point_coords(point: CurvePoint) -> tuple[int, int]: x, y = point.x, point.y if x is None or y is None: @@ -73,10 +67,8 @@ def _compute_quotient_and_linearization_terms( accx_value: int, accy_value: int, l_zeta_omega_value: int, - seed_x_value: int, - seed_y_value: int, - result_seed_x_value: int, - result_seed_y_value: int, + seed: tuple[int, int], + result_seed: tuple[int, int], domain_last_value: int, domain_neg1_value: int, domain_neg2_value: int, @@ -85,11 +77,12 @@ def _compute_quotient_and_linearization_terms( domain_size_inv_value: int, edwards_a_value: int, omega_value: int, + prime_value: int, ) -> tuple[int, int, int, int, int, int]: if len(alpha_list) < 7 or len(v_list) < 8: raise ValueError("expected at least 7 alpha values and 8 aggregation values") - prime = S_PRIME + prime = int(prime_value) alphas = [int(value) % prime for value in alpha_list[:7]] v_values = [int(value) % prime for value in v_list[:8]] zeta = int(zeta_value) % prime @@ -101,10 +94,10 @@ def _compute_quotient_and_linearization_terms( accx = int(accx_value) % prime accy = int(accy_value) % prime l_zeta_omega = int(l_zeta_omega_value) % prime - seed_x = int(seed_x_value) % prime - seed_y = int(seed_y_value) % prime - result_seed_x = int(result_seed_x_value) % prime - result_seed_y = int(result_seed_y_value) % prime + seed_x = int(seed[0]) % prime + seed_y = int(seed[1]) % prime + result_seed_x = int(result_seed[0]) % prime + result_seed_y = int(result_seed[1]) % prime domain_last = int(domain_last_value) % prime domain_neg1 = int(domain_neg1_value) % prime domain_neg2 = int(domain_neg2_value) % prime @@ -185,7 +178,7 @@ def _compute_quotient_and_linearization_terms( def linear_pcs_verifications( - proof: RingProofFields | tuple[Any, ...], + proof: RingProofFields, fixed_cols_blst: tuple[Any, Any, Any], relation_to_prove: CurvePoint, result_plus_seed: CurvePoint, @@ -195,11 +188,11 @@ def linear_pcs_verifications( domain_size_inv: int, edwards_a: int, omega: int, + prime: int, transcript_prefix: FiatShamirTranscript, witness_commitments: bytes, quotient_commitment: bytes, ) -> tuple[LinearPcsVerification, LinearPcsVerification]: - proof_fields = _ring_proof_fields(proof) result_plus_seed_coords = _point_coords(result_plus_seed) seed_coords = _point_coords(seed_point) @@ -208,8 +201,8 @@ def linear_pcs_verifications( relation_to_prove, witness_commitments, quotient_commitment, - proof_fields.evaluations, - proof_fields.l_zeta_omega, + proof.evaluations, + proof.l_zeta_omega, ) last_index = len(domain) - padding_rows @@ -217,18 +210,16 @@ def linear_pcs_verifications( alpha_list, v_list, zeta_p, - proof_fields.px_zeta, - proof_fields.py_zeta, - proof_fields.s_zeta, - proof_fields.b_zeta, - proof_fields.accip_zeta, - proof_fields.accx_zeta, - proof_fields.accy_zeta, - proof_fields.l_zeta_omega, - seed_coords[0], - seed_coords[1], - result_plus_seed_coords[0], - result_plus_seed_coords[1], + proof.px_zeta, + proof.py_zeta, + proof.s_zeta, + proof.b_zeta, + proof.accip_zeta, + proof.accx_zeta, + proof.accy_zeta, + proof.l_zeta_omega, + seed_coords, + result_plus_seed_coords, domain[last_index], domain[-1], domain[-2], @@ -237,17 +228,18 @@ def linear_pcs_verifications( domain_size_inv, edwards_a, omega, + prime, ) agg_zeta, scalar_accip, scalar_accx, scalar_accy, zeta_omega, l_zeta_omega_out = linear_eval_bundle Cpx_blst, Cpy_blst, Cs_blst = fixed_cols_blst - Cb_blst = g1_to_blst(proof_fields.c_b) - Caccip_blst = g1_to_blst(proof_fields.c_accip) - Caccx_blst = g1_to_blst(proof_fields.c_accx) - Caccy_blst = g1_to_blst(proof_fields.c_accy) - Cq_blst = g1_to_blst(proof_fields.c_q) - Phi_zeta_blst = g1_to_blst(proof_fields.phi_zeta) - Phi_zeta_omega_blst = g1_to_blst(proof_fields.phi_zeta_omega) + Cb_blst = g1_to_blst(proof.c_b) + Caccip_blst = g1_to_blst(proof.c_accip) + Caccx_blst = g1_to_blst(proof.c_accx) + Caccy_blst = g1_to_blst(proof.c_accy) + Cq_blst = g1_to_blst(proof.c_q) + Phi_zeta_blst = g1_to_blst(proof.phi_zeta) + Phi_zeta_omega_blst = g1_to_blst(proof.phi_zeta_omega) quotient_terms = ( (Cpx_blst, v_list[0]), @@ -271,14 +263,9 @@ def linear_pcs_verifications( class Verify: - prime = S_PRIME - pcs: type[PCS] = KZG - omega: int | None = None - edwards_a = -5 - def __init__( self, - proof: RingProofFields | tuple[Any, ...], + proof: RingProofFields, fixed_cols: list, relation_to_prove: CurvePoint, result_plus_seed: CurvePoint, @@ -287,29 +274,28 @@ def __init__( transcript_prefix: FiatShamirTranscript, padding_rows: int = 4, edwards_a: int = -5, - prime: int = S_PRIME, + prime: int | None = None, omega: int | None = None, pcs: type[PCS] = KZG, domain_size_inv: int | None = None, transcript_witness_commitments: tuple[Any, ...] | bytes | None = None, transcript_quotient_commitment: Any | None = None, ) -> None: - proof_fields = _ring_proof_fields(proof) - self.Cb = proof_fields.c_b - self.Caccip = proof_fields.c_accip - self.Caccx = proof_fields.c_accx - self.Caccy = proof_fields.c_accy - self.px_zeta = proof_fields.px_zeta - self.py_zeta = proof_fields.py_zeta - self.s_zeta = proof_fields.s_zeta - self.b_zeta = proof_fields.b_zeta - self.accip_zeta = proof_fields.accip_zeta - self.accx_zeta = proof_fields.accx_zeta - self.accy_zeta = proof_fields.accy_zeta - self.Cq = proof_fields.c_q - self.l_zeta_omega = proof_fields.l_zeta_omega - self.Phi_zeta = proof_fields.phi_zeta - self.Phi_zeta_omega = proof_fields.phi_zeta_omega + self.Cb = proof.c_b + self.Caccip = proof.c_accip + self.Caccx = proof.c_accx + self.Caccy = proof.c_accy + self.px_zeta = proof.px_zeta + self.py_zeta = proof.py_zeta + self.s_zeta = proof.s_zeta + self.b_zeta = proof.b_zeta + self.accip_zeta = proof.accip_zeta + self.accx_zeta = proof.accx_zeta + self.accy_zeta = proof.accy_zeta + self.Cq = proof.c_q + self.l_zeta_omega = proof.l_zeta_omega + self.Phi_zeta = proof.phi_zeta + self.Phi_zeta_omega = proof.phi_zeta_omega self.Cpx, self.Cpy, self.Cs = fixed_cols self.relation_to_prove = relation_to_prove @@ -317,7 +303,7 @@ def __init__( self.sp = _point_coords(seed_point) self.D = domain self.padding_rows = padding_rows - self.prime = prime + self.prime = prime if prime is not None else relation_to_prove.curve.params.field_modulus self.omega = omega self.pcs = pcs self.domain_size_inv = domain_size_inv if domain_size_inv is not None else pow(len(self.D), -1, self.prime) @@ -343,19 +329,19 @@ def __init__( witness_commitments = ( transcript_witness_commitments if transcript_witness_commitments is not None - else [self.pcs.serialize_g1_uncompressed(cmt) for cmt in proof_fields.witness_commitments] + else [self.pcs.serialize_g1_uncompressed(cmt) for cmt in proof.witness_commitments] ) # Add quotient and evaluations once we have their serialized form. quotient_commitment = ( - transcript_quotient_commitment if transcript_quotient_commitment is not None else self.pcs.serialize_g1_uncompressed(proof_fields.c_q) + transcript_quotient_commitment if transcript_quotient_commitment is not None else self.pcs.serialize_g1_uncompressed(proof.c_q) ) self.t, self.alpha_list, self.zeta_p, self.V_list = derive_challenges_after_vk( self.t, self.relation_to_prove, witness_commitments, quotient_commitment, - proof_fields.evaluations, - proof_fields.l_zeta_omega, + proof.evaluations, + proof.l_zeta_omega, ) def _domain_omega(self) -> int: @@ -364,7 +350,7 @@ def _domain_omega(self) -> int: domain_size = len(self.D) if 2048 % domain_size != 0: raise ValueError(f"omega must be supplied for domain size {domain_size}") - return pow(OMEGA_2048, 2048 // domain_size, self.prime) + return pow(ROOT_OF_UNITY_2048, 2048 // domain_size, self.prime) def is_valid(self) -> bool: """If both the verifications are true then sign is valid""" @@ -409,10 +395,8 @@ def _native_linear_eval_bundle(self) -> tuple[int, int, int, int, int, int]: self.accx_zeta, self.accy_zeta, self.l_zeta_omega, - self.sp[0], - self.sp[1], - self.Result_plus_Seed[0], - self.Result_plus_Seed[1], + self.sp, + self.Result_plus_Seed, self.D[self.last_index], self.D[-1], self.D[-2], @@ -421,5 +405,6 @@ def _native_linear_eval_bundle(self) -> tuple[int, int, int, int, int, int]: self.domain_size_inv, self.edwards_a, omega, + self.prime, ) return bundle diff --git a/dot_ring/vrf/delinearize.py b/dot_ring/vrf/delinearize.py deleted file mode 100644 index 3c9c90c..0000000 --- a/dot_ring/vrf/delinearize.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from dot_ring.curve.curve import CurveVariant - -from .domain import DomSep -from .transcript import CHALLENGE_LEN - -if TYPE_CHECKING: - from .transcript import VrfTranscript - - -class DelinearizeScalars: - def __init__(self, curve: CurveVariant, transcript: VrfTranscript) -> None: - self.curve = curve - self.transcript = transcript.copy() - self.transcript.absorb(bytes([DomSep.DELINEARIZE])) - self.first = True - - def next(self) -> int: - if self.first: - self.first = False - return 1 - return int.from_bytes(self.transcript.squeeze(CHALLENGE_LEN), "little") % self.curve.curve.params.subgroup_order - - def take(self, n: int) -> list[int]: - return [self.next() for _ in range(n)] diff --git a/dot_ring/vrf/ietf/thin.py b/dot_ring/vrf/ietf/thin.py index 19dfcdc..9597e5b 100644 --- a/dot_ring/vrf/ietf/thin.py +++ b/dot_ring/vrf/ietf/thin.py @@ -1,12 +1,17 @@ +"""Thin VRF (section 3). + +The library proof envelope is `gamma || R || s`; the spec proof is `R || s`. +""" + from __future__ import annotations -import secrets from dataclasses import dataclass from typing import Any from dot_ring.curve.curve import CurveVariant from dot_ring.curve.point import CurvePoint from dot_ring.vrf.transcript import ( + CHALLENGE_LEN, DomSep, VrfIo, challenge, @@ -17,6 +22,8 @@ scalar_encode, scalar_len, schnorr_ios, + squeeze_transcript_bytes, + suite_id, vrf_transcript, vrf_transcript_scalars, ) @@ -35,7 +42,7 @@ class ThinBatchItem: @dataclass class ThinVRF(VRF[Any]): - """Batch-friendly VRF-AD proof storing the nonce commitment.""" + """Batch-friendly VRF-AD proof. Fields after `output_point` are spec proof `(R, s)`.""" output_point: CurvePoint r: CurvePoint @@ -56,6 +63,8 @@ def decode(cls, proof_bytes: bytes) -> ThinVRF: s = scalar_decode(cls.cv, proof_bytes[2 * encoded_point_len :]) if s >= cls.cv.curve.params.subgroup_order: raise ValueError("Response scalar s is not less than the curve order") + if not (cls._valid_point(output_point) and cls._valid_point(r)): + raise ValueError("Invalid identity or subgroup point in proof") return cls(output_point, r, s) def encode(self) -> bytes: @@ -83,6 +92,7 @@ def prove_ios( public_key: CurvePoint, additional_data: bytes, ) -> ThinVRF: + """Spec section 3.1 steps 2-7 over caller-supplied I/O pairs.""" transcript, merged = vrf_transcript(cls.cv, DomSep.THIN_VRF, schnorr_ios(cls.cv, public_key, ios), additional_data) k = nonce(cls.cv, secret_scalar, transcript) r = merged.input * k @@ -100,6 +110,11 @@ def verify(self, public_key: bytes, input: bytes, additional_data: bytes, salt: return self.verify_ios(public_key_pt, [VrfIo(input_point, self.output_point)], additional_data) def verify_ios(self, public_key: CurvePoint, ios: list[VrfIo], additional_data: bytes) -> bool: + """Spec section 3.2: validate inputs, reconstruct `T`, derive `c`, check `s*I_m = R + c*O_m`.""" + proof_points_valid = self._valid_point(public_key) and self._valid_point(self.r) + ios_valid = all(self._valid_point(io.input) and self._valid_point(io.output) for io in ios) + if not proof_points_valid or not ios_valid: + return False transcript, merged = vrf_transcript(self.cv, DomSep.THIN_VRF, schnorr_ios(self.cv, public_key, ios), additional_data) c = challenge(self.cv, [self.r], transcript) lhs = self.cv.msm([merged.input, merged.output], [self.s, -c]) @@ -112,22 +127,29 @@ def proof_to_hash(cls, gamma: CurvePoint, mul_cofactor: bool = False) -> bytes: return point_to_hash(cls.cv, gamma) -def _batch_coefficients(count: int, order: int) -> list[int]: - if count == 0: +def _batch_coefficients(items: list[ThinBatchItem], curve: CurveVariant) -> list[int]: + """Spec section 3.3.2: `suite_id || BatchVerify || enc_scalar(c_j) || enc_scalar(s_j)`.""" + if not items: return [] - coefficients = [1] - for _ in range(count - 1): - coefficient = 0 - while coefficient == 0: - coefficient = secrets.randbelow(order) - coefficients.append(coefficient) - return coefficients + + absorbed = bytearray(suite_id(curve)) + absorbed.append(DomSep.BATCH_VERIFY) + for item in items: + absorbed.extend(scalar_encode(curve, item.c)) + absorbed.extend(scalar_encode(curve, item.s)) + + order = curve.curve.params.subgroup_order + raw = squeeze_transcript_bytes(curve.curve.params.hash_fn, bytes(absorbed), CHALLENGE_LEN * len(items)) + return [int.from_bytes(raw[CHALLENGE_LEN * index : CHALLENGE_LEN * (index + 1)], "little") % order for index in range(len(items))] class ThinBatchVerifier: + """Accumulates spec section 3.3 items and verifies the weighted Thin equation.""" + def __init__(self, curve: CurveVariant): self.cv = curve self.items: list[ThinBatchItem] = [] + self._invalid = False @classmethod def __class_getitem__(cls, curve_variant: CurveVariant | Any) -> type[ThinBatchVerifier] | Any: @@ -142,19 +164,25 @@ def __init__(self) -> None: return _SpecializedThinBatchVerifier def push(self, public_key: CurvePoint, ios: list[VrfIo], additional_data: bytes, proof: ThinVRF) -> None: + proof_points_valid = VRF._valid_point(public_key) and VRF._valid_point(proof.r) + ios_valid = all(VRF._valid_point(io.input) and VRF._valid_point(io.output) for io in ios) + if not proof_points_valid or not ios_valid: + self._invalid = True + return chained_ios = schnorr_ios(self.cv, public_key, ios) transcript, scalar_stream = vrf_transcript_scalars(self.cv, DomSep.THIN_VRF, chained_ios, additional_data) c = challenge(self.cv, [proof.r], transcript) - self.items.append(ThinBatchItem(c, chained_ios, scalar_stream.take(len(chained_ios)), proof.r, proof.s)) + self.items.append(ThinBatchItem(c, chained_ios, scalar_stream, proof.r, proof.s)) def verify(self) -> bool: + if self._invalid: + return False if not self.items: return True points: list[CurvePoint] = [] scalars: list[int] = [] - order = self.cv.curve.params.subgroup_order - for coefficient, item in zip(_batch_coefficients(len(self.items), order), self.items, strict=False): + for coefficient, item in zip(_batch_coefficients(self.items, self.cv), self.items, strict=True): weighted_c = coefficient * item.c weighted_s = coefficient * item.s diff --git a/dot_ring/vrf/ietf/tiny.py b/dot_ring/vrf/ietf/tiny.py index 6b1d8bc..761149c 100644 --- a/dot_ring/vrf/ietf/tiny.py +++ b/dot_ring/vrf/ietf/tiny.py @@ -1,3 +1,8 @@ +"""Tiny VRF (section 2). + +The library proof envelope is `gamma || c || s`; the spec proof is `c || s`. +""" + from __future__ import annotations from dataclasses import dataclass @@ -25,18 +30,7 @@ @dataclass class TinyVRF(VRF[Any]): - """ - Tiny VRF proof. - - Compact VRF-AD proof using transcript-derived challenges and nonces, - additional data, and multi-input delinearization. - - Usage: - >>> from dot_ring.curve.specs.bandersnatch import Bandersnatch - >>> from dot_ring.vrf.ietf import TinyVRF - >>> proof = TinyVRF[Bandersnatch].prove(alpha, secret_key, additional_data) - >>> verified = proof.verify(public_key, input_point, additional_data) - """ + """Compact VRF-AD proof. Fields after `output_point` are spec proof `(c, s)`.""" output_point: CurvePoint c: int @@ -58,6 +52,8 @@ def decode(cls, proof_bytes: bytes) -> TinyVRF: s = scalar_decode(cls.cv, proof_bytes[encoded_point_len + CHALLENGE_LEN :]) if s >= order: raise ValueError("Response scalar s is not less than the curve order") + if not cls._valid_point(output_point): + raise ValueError("Invalid identity or subgroup point in proof") return cls(output_point, c, s) def encode(self) -> bytes: @@ -90,6 +86,7 @@ def prove_ios( public_key: CurvePoint, additional_data: bytes, ) -> TinyVRF: + """Spec section 2.1 steps 2-7 over caller-supplied I/O pairs.""" if len(ios) == 1: transcript, scalars = vrf_transcript_scalars( cls.cv, @@ -97,8 +94,7 @@ def prove_ios( schnorr_ios(cls.cv, public_key, ios), additional_data, ) - z0 = scalars.next() - z1 = scalars.next() + z0, z1 = scalars k = nonce(cls.cv, secret_scalar, transcript) generator = cls.cv.generator_point() order = cls.cv.curve.params.subgroup_order @@ -128,6 +124,10 @@ def verify(self, public_key: bytes, input: bytes, additional_data: bytes, salt: return self.verify_ios(public_key_pt, [io], additional_data) def verify_ios(self, public_key: CurvePoint, ios: list[VrfIo], additional_data: bytes) -> bool: + """Spec section 2.2: validate inputs, reconstruct `R = s*I_m - c*O_m`, then compare challenges.""" + ios_valid = all(self._valid_point(io.input) and self._valid_point(io.output) for io in ios) + if not self._valid_point(public_key) or not ios_valid: + return False if len(ios) == 1: transcript, scalars = vrf_transcript_scalars( self.cv, @@ -135,8 +135,7 @@ def verify_ios(self, public_key: CurvePoint, ios: list[VrfIo], additional_data: schnorr_ios(self.cv, public_key, ios), additional_data, ) - z0 = scalars.next() - z1 = scalars.next() + z0, z1 = scalars generator = self.cv.generator_point() order = self.cv.curve.params.subgroup_order r = self.cv.msm( @@ -183,6 +182,8 @@ def ecvrf_decode_proof(cls, proof: bytes | str) -> tuple[CurvePoint, int, int]: s = scalar_decode(cls.cv, proof[encoded_point_len + CHALLENGE_LEN :]) if s >= order: raise ValueError("Response scalar S is not less than the curve order") + if not cls._valid_point(output_point): + raise ValueError("Invalid identity or subgroup point in proof") return output_point, c, s @classmethod diff --git a/dot_ring/vrf/pedersen/batch_verifier.py b/dot_ring/vrf/pedersen/batch_verifier.py index 6dc76de..fe27c78 100644 --- a/dot_ring/vrf/pedersen/batch_verifier.py +++ b/dot_ring/vrf/pedersen/batch_verifier.py @@ -1,3 +1,5 @@ +"""Pedersen VRF batch verification from Bandersnatch VRF spec section 4.4.""" + from __future__ import annotations from dataclasses import dataclass @@ -65,20 +67,21 @@ def _fold_duplicate_points_by_identity(points: list[CurvePoint], scalars: list[i def _batch_coefficient_pairs_from_bytes(item_count: int, item_bytes: bytes | bytearray, curve: CurveVariant) -> list[tuple[int, int]]: + """Spec section 4.4.2: transcript weights from scalar triples only, in item order.""" if item_count == 0: return [] absorbed = bytearray(suite_id(curve)) absorbed.append(DomSep.BATCH_VERIFY) - absorbed.extend(item_count.to_bytes(8, "little")) absorbed.extend(item_bytes) + order = curve.curve.params.subgroup_order raw = squeeze_transcript_bytes(curve.curve.params.hash_fn, bytes(absorbed), 2 * CHALLENGE_LEN * item_count) pairs = [] for index in range(item_count): offset = 2 * CHALLENGE_LEN * index - io_weight = int.from_bytes(raw[offset : offset + CHALLENGE_LEN], "little") or 1 - commitment_weight = int.from_bytes(raw[offset + CHALLENGE_LEN : offset + 2 * CHALLENGE_LEN], "little") or 1 + io_weight = int.from_bytes(raw[offset : offset + CHALLENGE_LEN], "little") % order + commitment_weight = int.from_bytes(raw[offset + CHALLENGE_LEN : offset + 2 * CHALLENGE_LEN], "little") % order pairs.append((io_weight, commitment_weight)) return pairs @@ -101,6 +104,8 @@ def _batch_coefficient_pairs(items: list[_PedersenBatchItem], curve: CurveVarian class PedersenBatchVerifier: + """Accumulates spec section 4.4 items and verifies the two weighted equations together.""" + def __init__(self, curve: CurveVariant): self.cv = curve self.items: list[_PedersenBatchItem] = [] @@ -108,6 +113,7 @@ def __init__(self, curve: CurveVariant): self._scalar_size = (self._order.bit_length() + 7) // 8 self._coefficient_item_bytes = bytearray() self._single_io_items = True + self._invalid = False @classmethod def __class_getitem__(cls, curve_variant: CurveVariant | Any) -> type[PedersenBatchVerifier] | Any: @@ -121,17 +127,34 @@ def __init__(self) -> None: _SpecializedPedersenBatchVerifier.__name__ = f"{cls.__name__}[{curve_variant.name}]" return _SpecializedPedersenBatchVerifier - def _item(self, ios: list[VrfIo], additional_data: bytes, proof: PedersenVRF) -> _PedersenBatchItem: + def _item( + self, + ios: list[VrfIo], + additional_data: bytes, + proof: PedersenVRF, + ) -> _PedersenBatchItem: + if not PedersenVRF._valid_ios(ios): + return self._invalid_item(proof) + return self._item_from_trusted_ios(ios, additional_data, proof) + + def _item_from_trusted_ios( + self, + ios: list[VrfIo], + additional_data: bytes, + proof: PedersenVRF, + ) -> _PedersenBatchItem: + if not proof._valid_proof_points(): + return self._invalid_item(proof) if len(ios) == 1: return self._single_io_item(ios[0], additional_data, proof) - transcript, scalar_stream = vrf_transcript_scalars(self.cv, DomSep.PEDERSEN_VRF, ios, additional_data) + transcript, zs = vrf_transcript_scalars(self.cv, DomSep.PEDERSEN_VRF, ios, additional_data) transcript.absorb(proof.blinded_pk.point_to_string()) c = challenge(self.cv, [proof.result_point, proof.ok], transcript) return _PedersenBatchItem( c=c, ios=list(ios), - zs=scalar_stream.take(len(ios)), + zs=zs, pk_com=proof.blinded_pk, r=proof.result_point, ok=proof.ok, @@ -139,6 +162,10 @@ def _item(self, ios: list[VrfIo], additional_data: bytes, proof: PedersenVRF) -> sb=proof.sb, ) + def _invalid_item(self, proof: PedersenVRF) -> _PedersenBatchItem: + self._invalid = True + return _PedersenBatchItem(0, [], [], proof.blinded_pk, proof.result_point, proof.ok, proof.s, proof.sb) + def _single_io_item(self, io: VrfIo, additional_data: bytes, proof: PedersenVRF) -> _PedersenBatchItem: absorbed = bytearray(suite_id(self.cv)) absorbed.append(DomSep.PEDERSEN_VRF) @@ -164,6 +191,8 @@ def _single_io_item(self, io: VrfIo, additional_data: bytes, proof: PedersenVRF) ) def _push_item(self, item: _PedersenBatchItem) -> None: + if self._invalid: + return self.items.append(item) self._single_io_items = self._single_io_items and len(item.ios) == 1 and len(item.zs) == 1 and item.zs[0] == 1 self._coefficient_item_bytes.extend(_batch_coefficient_item_bytes(item, self._scalar_size, self._order)) @@ -172,6 +201,8 @@ def push(self, ios: list[VrfIo], additional_data: bytes, proof: PedersenVRF) -> self._push_item(self._item(ios, additional_data, proof)) def verify(self) -> bool: + if self._invalid: + return False if not self.items: return True diff --git a/dot_ring/vrf/pedersen/vrf.py b/dot_ring/vrf/pedersen/vrf.py index dde04bf..99d73d5 100644 --- a/dot_ring/vrf/pedersen/vrf.py +++ b/dot_ring/vrf/pedersen/vrf.py @@ -1,6 +1,12 @@ +"""Pedersen VRF (section 4). + +The library proof envelope is `gamma || Y_bar || R || O_k || s || s_b`; +the spec proof is `Y_bar || R || O_k || s || s_b`. +""" + from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, TypeVar from dot_ring.curve.curve import CurveVariant @@ -30,20 +36,9 @@ def _blinding_base(curve: CurveVariant) -> CurvePoint: return curve.point(blinding_base[0], blinding_base[1]) -@dataclass +@dataclass(frozen=True) class PedersenVRF(VRF[C]): - """ - Pedersen VRF implementation. - - This implementation provides Pedersen-style VRF operations with blinding - support. - - Usage: - >>> from dot_ring.curve.specs.bandersnatch import Bandersnatch - >>> from dot_ring.vrf.pedersen import PedersenVRF - >>> proof = PedersenVRF[Bandersnatch].prove(alpha, secret_key, additional_data) - >>> verified = PedersenVRF[Bandersnatch].verify(input_point, additional_data, proof) - """ + """Pedersen VRF proof plus gamma envelope. `_blinding_factor` is prover-local for Ring VRF.""" output_point: CurvePoint blinded_pk: CurvePoint @@ -52,6 +47,7 @@ class PedersenVRF(VRF[C]): s: int sb: int _blinding_factor: int = 0 + _points_validated: bool = field(default=False, repr=False, compare=False) @classmethod def proof_len(cls) -> int: @@ -80,6 +76,8 @@ def decode(cls, proof: bytes) -> PedersenVRF: raise ValueError("Response scalar s is not canonical") if sb >= order: raise ValueError("Response scalar sb is not canonical") + if not all(cls._valid_point(point) for point in (output_point, public_key_cp, r, ok)): + raise ValueError("Invalid identity or subgroup point in proof") return cls( output_point=output_point, @@ -88,6 +86,7 @@ def decode(cls, proof: bytes) -> PedersenVRF: ok=ok, s=s, sb=sb, + _points_validated=True, ) def encode(self) -> bytes: @@ -117,7 +116,7 @@ def prove( secret_scalar = scalar_decode(cls.cv, secret_key) public_key = cls.cv.generator_point() * secret_scalar io = cls._io_from_alpha(alpha, secret_scalar, salt) - return cls.prove_ios([io], secret_scalar, public_key, additional_data) + return cls._prove_ios([io], secret_scalar, public_key, additional_data, points_validated=True) @classmethod def prove_ios( @@ -127,6 +126,19 @@ def prove_ios( public_key: CurvePoint, additional_data: bytes, ) -> PedersenVRF: + return cls._prove_ios(ios, secret_scalar, public_key, additional_data, points_validated=False) + + @classmethod + def _prove_ios( + cls, + ios: list[VrfIo], + secret_scalar: int, + public_key: CurvePoint, + additional_data: bytes, + *, + points_validated: bool, + ) -> PedersenVRF: + """Spec section 4.1 steps 1-10 over caller-supplied I/O pairs.""" transcript, merged = vrf_transcript(cls.cv, DomSep.PEDERSEN_VRF, ios, additional_data) blinding_factor = cls.blinding_scalar(secret_scalar, transcript) blinding_base = _blinding_base(cls.cv) @@ -153,13 +165,40 @@ def prove_ios( s=s, sb=sb, _blinding_factor=blinding_factor, + _points_validated=points_validated, ) def verify(self, input: bytes, additional_data: bytes, salt: bytes = b"") -> bool: + if not self._valid_proof_points(): + return False input_point = self.cv.encode_to_curve(input, salt) - return self.verify_ios([VrfIo(input_point, self.output_point)], additional_data) + return self._verify_ios([VrfIo(input_point, self.output_point)], additional_data) def verify_ios(self, ios: list[VrfIo], additional_data: bytes) -> bool: + """Spec section 4.2: validate points, rebuild `T`, then check VRF and commitment equations.""" + if not self._valid_proof_points() or not self._valid_ios(ios): + return False + return self._verify_ios(ios, additional_data) + + def _valid_proof_points(self) -> bool: + if self._points_validated: + return True + return all(self._valid_point(point) for point in (self.output_point, self.blinded_pk, self.result_point, self.ok)) + + @classmethod + def _valid_ios(cls, ios: list[VrfIo]) -> bool: + seen: set[int] = set() + for io in ios: + for point in (io.input, io.output): + point_id = id(point) + if point_id in seen: + continue + seen.add(point_id) + if not cls._valid_point(point): + return False + return True + + def _verify_ios(self, ios: list[VrfIo], additional_data: bytes) -> bool: transcript, merged = vrf_transcript(self.cv, DomSep.PEDERSEN_VRF, ios, additional_data) transcript.absorb(self.blinded_pk.point_to_string()) c = challenge(self.cv, [self.result_point, self.ok], transcript) @@ -173,6 +212,21 @@ def verify_ios(self, ios: list[VrfIo], additional_data: bytes) -> bool: lhs2 = self.cv.msm([generator, blinding_base, self.blinded_pk], [self.s, self.sb, -c]) return lhs2 == self.result_point + def verify_unblinding(self, public_key: bytes | str | CurvePoint, blinding_factor: int) -> bool: + """Spec section 4.3: check `Y_bar = Y + b*B` for a revealed blinding factor.""" + order = self.cv.curve.params.subgroup_order + if not 0 <= blinding_factor < order: + return False + if isinstance(public_key, bytes | str): + try: + public_key = self.cv.string_to_point(public_key) + except ValueError as exc: + raise ValueError("Invalid public key") from exc + blinded_pk_valid = self._points_validated or self._valid_point(self.blinded_pk) + if not (self._valid_point(public_key) and blinded_pk_valid): + return False + return public_key + _blinding_base(self.cv) * blinding_factor == self.blinded_pk + @classmethod def blinding_scalar(cls, secret_scalar: int, transcript: Any) -> int: t = transcript.copy() diff --git a/dot_ring/vrf/ring/batch_verifier.py b/dot_ring/vrf/ring/batch_verifier.py index 90ac6ba..ed4dd76 100644 --- a/dot_ring/vrf/ring/batch_verifier.py +++ b/dot_ring/vrf/ring/batch_verifier.py @@ -107,7 +107,7 @@ def ring_pcs_verifications(self, proof: RingVRF, message: Any) -> tuple[LinearPc rltn, res_plus_seed = _proof_relation_points(proof, message, ring.params) witness_commitments, quotient_commitment = _proof_transcript_commitments(proof, ring.params) return linear_pcs_verifications( - proof.ring_proof_tuple(), + proof.as_ring_proof(), self.fixed_cols_blst, rltn, res_plus_seed, @@ -117,6 +117,7 @@ def ring_pcs_verifications(self, proof: RingVRF, message: Any) -> tuple[LinearPc self.domain_size_inv, self.edwards_a, self.omega, + ring.params.prime, self.transcript_prefix, witness_commitments, quotient_commitment, @@ -126,15 +127,29 @@ def _item_from_ios(self, proof: RingVRF, ios: list[VrfIo], ad: bytes) -> _RingBa return _ring_batch_item(proof, ios, ad, self) def _item(self, proof: RingVRF, input: bytes, ad: bytes) -> _RingBatchItem: - pedersen_proof = proof.require_pedersen_proof() + pedersen_proof = proof.pedersen_proof input_point = proof.cv.encode_to_curve(input) - return self._item_from_ios(proof, [VrfIo(input_point, pedersen_proof.output_point)], ad) + return _ring_batch_item_from_trusted_ios(proof, [VrfIo(input_point, pedersen_proof.output_point)], ad, self) def _pedersen_item(self, proof: RingVRF, ios: list[VrfIo], ad: bytes) -> Any: - pedersen_proof = proof.require_pedersen_proof() + pedersen_proof = proof.pedersen_proof + if self.ring.params.cv.name != proof.cv.name: + raise ValueError("proof curve does not match ring curve") + verifier = PedersenBatchVerifier(proof.cv) + item = verifier._item(ios, ad, pedersen_proof) + if verifier._invalid: + raise ValueError("invalid Pedersen VRF point") + return item + + def _pedersen_item_from_trusted_ios(self, proof: RingVRF, ios: list[VrfIo], ad: bytes) -> Any: + pedersen_proof = proof.pedersen_proof if self.ring.params.cv.name != proof.cv.name: raise ValueError("proof curve does not match ring curve") - return PedersenBatchVerifier(proof.cv)._item(ios, ad, pedersen_proof) + verifier = PedersenBatchVerifier(proof.cv) + item = verifier._item_from_trusted_ios(ios, ad, pedersen_proof) + if verifier._invalid: + raise ValueError("invalid Pedersen VRF point") + return item def _pcs_item(self, proof: RingVRF, message: Any) -> tuple[Any, tuple[LinearPcsVerification, LinearPcsVerification]]: pcs = self.ring.params.pcs @@ -144,7 +159,13 @@ def _pcs_item(self, proof: RingVRF, message: Any) -> tuple[Any, tuple[LinearPcsV def _ring_batch_item(proof: RingVRF, ios: list[VrfIo], ad: bytes, context: _RingBatchContext) -> _RingBatchItem: pedersen_item = context._pedersen_item(proof, ios, ad) - pcs, verifications = context._pcs_item(proof, proof.require_pedersen_proof().blinded_pk) + pcs, verifications = context._pcs_item(proof, proof.pedersen_proof.blinded_pk) + return _RingBatchItem(proof.cv, pedersen_item, pcs, verifications) + + +def _ring_batch_item_from_trusted_ios(proof: RingVRF, ios: list[VrfIo], ad: bytes, context: _RingBatchContext) -> _RingBatchItem: + pedersen_item = context._pedersen_item_from_trusted_ios(proof, ios, ad) + pcs, verifications = context._pcs_item(proof, proof.pedersen_proof.blinded_pk) return _RingBatchItem(proof.cv, pedersen_item, pcs, verifications) diff --git a/dot_ring/vrf/ring/members.py b/dot_ring/vrf/ring/members.py index 50561fa..23223ef 100644 --- a/dot_ring/vrf/ring/members.py +++ b/dot_ring/vrf/ring/members.py @@ -1,3 +1,9 @@ +"""Ring member preprocessing for the ring-proof public columns (section 3.1). + +Invalid or identity keys are normalized to the configured padding point before +fixed columns are built; producer-key lookup remains strict. +""" + from __future__ import annotations from collections.abc import Sequence @@ -19,13 +25,7 @@ class Ring: params: RingProofParams def __init__(self, keys: Sequence[bytes | str], params: RingProofParams | None = None) -> None: - """ - Initialize a Ring from a list of public keys. - - Args: - keys: List of public keys (as bytes) for ring members - params: Ring proof parameters. If None, automatically constructed based on ring size. - """ + """Build spec public vector `PK || B powers || zero padding` from encoded keys.""" if params is None: params = RingProofParams.from_ring_size(len(keys)) diff --git a/dot_ring/vrf/ring/proof_builder.py b/dot_ring/vrf/ring/proof_builder.py index 4f32160..3da6a69 100644 --- a/dot_ring/vrf/ring/proof_builder.py +++ b/dot_ring/vrf/ring/proof_builder.py @@ -1,3 +1,5 @@ +"""Ring-proof prover pipeline from ring-proof spec sections 3.1-3.3.""" + from __future__ import annotations from collections.abc import Sequence @@ -17,6 +19,8 @@ class RingProofBuilder: + """Build the SNARK payload that proves `producer_key + b*B` is in the committed ring.""" + def __init__( self, curve: CurveVariant, @@ -34,10 +38,12 @@ def __init__( self.transcript_challenge = transcript_challenge or curve.curve.params.suite_id def build(self) -> RingProofPayload: + """Spec sections 3.1-3.3 in order: witness, constraints, quotient, linearization, openings.""" params = self.ring.params ring_root = self.ring_root or RingRoot.from_ring(self.ring, params) producer_index = self.ring.index_of(self.producer_key) + # Section 3.1: witness bit column and accumulators for R = PK_k + bB. witness_builder = WitnessColumnBuilder.from_params( self.ring.nm_points, ring_root.s.evals, @@ -51,6 +57,7 @@ def build(self) -> RingProofPayload: relation_plus_seed_point = relation_point + params.cv.point(seed_point) relation_plus_seed = int(relation_plus_seed_point.x), int(relation_plus_seed_point.y) + # Section 3.2: c1-c7 constraints over fixed and witness columns. constraints = RingConstraintBuilder( result_plus_seed=relation_plus_seed, px=self._coeffs(ring_root.px), @@ -66,6 +73,7 @@ def build(self) -> RingProofPayload: constraint_polys = list(constraints.compute().values()) c_agg = self._aggregate_constraints(constraint_polys, alpha) + # Section 3.3.2: quotient commitment. q_poly = poly_divide_by_vanishing(c_agg, params.domain_size) c_q = params.pcs.commit(q_poly) c_q_column = Column(name="C_q", evals=[], commitment=c_q) @@ -103,6 +111,7 @@ def build(self) -> RingProofPayload: ) = relation_evals.values() c_b, c_accx, c_accy, c_accip = witness_columns + # Section 3.3.8: fixed payload order used by verifier. return RingProofPayload( c_b=c_b, c_accip=c_accip, @@ -147,6 +156,7 @@ def _aggregate_constraints( constraint_evals: Sequence[Sequence[int]], alphas: Sequence[int], ) -> list[int]: + """Spec section 3.3.1: alpha-combine constraint evaluations and hide final ZK rows.""" params = self.ring.params if not constraint_evals: return [] @@ -181,6 +191,7 @@ def _linearization_poly( witness_columns: tuple[Column, Column, Column, Column], alphas: Sequence[int], ) -> tuple[FiatShamirTranscript, int, dict[str, int], list[int], int, int]: + """Spec sections 3.3.3-3.3.5: choose zeta, evaluate relation columns, build L(X).""" params = self.ring.params current_transcript, zeta = phase2_eval_point( transcript, @@ -270,6 +281,7 @@ def _aggregated_opening_poly( q_poly: list[int], opening_challenges: Sequence[int], ) -> list[int]: + """Spec section 3.3.6: aggregate all zeta openings with the nu challenge vector.""" params = self.ring.params c_b, c_accx, c_accy, c_accip = witness_columns polynomials = [ diff --git a/dot_ring/vrf/ring/proof_payload.py b/dot_ring/vrf/ring/proof_payload.py index c7e5978..c1eab0d 100644 --- a/dot_ring/vrf/ring/proof_payload.py +++ b/dot_ring/vrf/ring/proof_payload.py @@ -8,28 +8,6 @@ RING_SCALAR_LEN = 32 - -def ring_proof_len(params: RingProofParams) -> int: - return 7 * params.pcs.commitment_size + 8 * RING_SCALAR_LEN - - -def encode_scalar(value: int) -> bytes: - return value.to_bytes(RING_SCALAR_LEN, "little") - - -def decode_scalar(data: bytes, modulus: int) -> int: - if len(data) != RING_SCALAR_LEN: - raise ValueError(f"scalar must be exactly {RING_SCALAR_LEN} bytes, got {len(data)}") - value = int.from_bytes(data, "little") - if value >= modulus: - raise ValueError("scalar is not canonical") - return value - - -def _commitment_column(name: str, commitment: G1Commitment) -> Column: - return Column(name=name, evals=[], commitment=commitment) - - @dataclass(slots=True) class RingProofPayload: c_b: Column @@ -87,7 +65,7 @@ def as_tuple( @staticmethod def encoded_len(params: RingProofParams) -> int: - return ring_proof_len(params) + return 7 * params.pcs.commitment_size + 8 * RING_SCALAR_LEN def encode(self, params: RingProofParams) -> bytes: return b"".join( @@ -96,15 +74,15 @@ def encode(self, params: RingProofParams) -> bytes: params.pcs.compress_g1(require_commitment(self.c_accip)), params.pcs.compress_g1(require_commitment(self.c_accx)), params.pcs.compress_g1(require_commitment(self.c_accy)), - encode_scalar(self.px_zeta), - encode_scalar(self.py_zeta), - encode_scalar(self.s_zeta), - encode_scalar(self.b_zeta), - encode_scalar(self.accip_zeta), - encode_scalar(self.accx_zeta), - encode_scalar(self.accy_zeta), + (self.px_zeta).to_bytes(RING_SCALAR_LEN, "little"), + (self.py_zeta).to_bytes(RING_SCALAR_LEN, "little"), + (self.s_zeta).to_bytes(RING_SCALAR_LEN, "little"), + (self.b_zeta).to_bytes(RING_SCALAR_LEN, "little"), + (self.accip_zeta).to_bytes(RING_SCALAR_LEN, "little"), + (self.accx_zeta).to_bytes(RING_SCALAR_LEN, "little"), + (self.accy_zeta).to_bytes(RING_SCALAR_LEN, "little"), params.pcs.compress_g1(require_commitment(self.c_q)), - encode_scalar(self.l_zeta_omega), + (self.l_zeta_omega).to_bytes(RING_SCALAR_LEN, "little"), params.pcs.compress_g1(self.open_agg_zeta), params.pcs.compress_g1(self.open_l_zeta_omega), ) @@ -112,16 +90,16 @@ def encode(self, params: RingProofParams) -> bytes: @classmethod def decode(cls, proof: bytes, params: RingProofParams) -> RingProofPayload: - expected = ring_proof_len(params) + expected = cls.encoded_len(params) if len(proof) != expected: raise ValueError(f"invalid Ring VRF proof length: expected {expected}, got {len(proof)}") reader = _PayloadReader(proof, params) payload = cls( - c_b=_commitment_column("c_b", reader.commitment()), - c_accip=_commitment_column("c_accip", reader.commitment()), - c_accx=_commitment_column("c_accx", reader.commitment()), - c_accy=_commitment_column("c_accy", reader.commitment()), + c_b=Column(name="c_b", evals=[], commitment=reader.commitment()), + c_accip=Column(name="c_accip", evals=[], commitment=reader.commitment()), + c_accx=Column(name="c_accx", evals=[], commitment=reader.commitment()), + c_accy=Column(name="c_accy", evals=[], commitment=reader.commitment()), px_zeta=reader.scalar(), py_zeta=reader.scalar(), s_zeta=reader.scalar(), @@ -129,7 +107,7 @@ def decode(cls, proof: bytes, params: RingProofParams) -> RingProofPayload: accip_zeta=reader.scalar(), accx_zeta=reader.scalar(), accy_zeta=reader.scalar(), - c_q=_commitment_column("c_q", reader.commitment()), + c_q=Column(name="c_q", evals=[], commitment=reader.commitment()), l_zeta_omega=reader.scalar(), open_agg_zeta=reader.commitment(), open_l_zeta_omega=reader.commitment(), @@ -153,7 +131,9 @@ def commitment(self) -> G1Commitment: def scalar(self) -> int: end = self.offset + RING_SCALAR_LEN - value = decode_scalar(self.data[self.offset : end], self.params.prime) + value = int.from_bytes(self.data[self.offset : end], "little") + if value >= self.params.prime: + raise ValueError("scalar is not canonical") self.offset = end return value diff --git a/dot_ring/vrf/ring/vrf.py b/dot_ring/vrf/ring/vrf.py index 410559d..5c30427 100644 --- a/dot_ring/vrf/ring/vrf.py +++ b/dot_ring/vrf/ring/vrf.py @@ -1,3 +1,9 @@ +"""Ring VRF (section 5). + +The encoded proof is the Pedersen library envelope followed by the ring-proof +payload; the spec writes this as `(pi_p, pi_r)`. +""" + from __future__ import annotations from dataclasses import dataclass @@ -22,22 +28,9 @@ @dataclass class RingVRF(VRF[Any]): - """ - Ring VRF implementation. - - This implementation provides Ring VRF operations combining - Pedersen VRF proofs with ring signatures. - - Usage: - >>> from dot_ring.curve.specs.bandersnatch import Bandersnatch - >>> from dot_ring.vrf.ring import RingVRF - >>> proof = RingVRF[Bandersnatch].prove(alpha, ad, secret_key, producer_key, keys) - >>> verified = RingVRF[Bandersnatch].verify(ad, ring_root, proof) + """Pedersen VRF proof plus ring proof that proves `Y_bar` belongs to the ring.""" - Note: Ring VRF currently only supports Bandersnatch curve. - """ - - pedersen_proof: PedersenVRF | None + pedersen_proof: PedersenVRF c_b: Column c_accip: Column c_accx: Column @@ -55,44 +48,25 @@ class RingVRF(VRF[Any]): open_l_zeta_omega: G1Commitment @classmethod - def proof_len(cls, skip_pedersen: bool = False) -> int: + def proof_len(cls) -> int: params = RingProofParams(cv=cls.cv) - proof_len = RingProofPayload.encoded_len(params) - if skip_pedersen: - return proof_len - return PedersenVRF[cls.cv].proof_len() + proof_len + return PedersenVRF[cls.cv].proof_len() + RingProofPayload.encoded_len(params) def encode(self) -> bytes: - """ - Serialize the Ring VRF proof to bytes. - - Returns: - bytes: Bytes representation of the Ring VRF proof - """ + """Serialize Pedersen + RingProof""" params = RingProofParams(cv=self.cv) - return self.require_pedersen_proof().encode() + self._payload().encode(params) + return self.pedersen_proof.encode() + self._payload().encode(params) @classmethod - def decode(cls, proof: bytes, skip_pedersen: bool = False) -> RingVRF: - """ - Deserialize the Ring VRF proof from bytes. - - Args: - proof: Bytes representation of the Ring VRF proof - Returns: - RingVRF: Deserialized Ring VRF proof object - """ - expected_len = cls.proof_len(skip_pedersen) + def decode(cls, proof: bytes) -> RingVRF: + """Decode the Pedersen and the fixed-order ring-proof payload.""" + expected_len = cls.proof_len() if len(proof) != expected_len: raise ValueError(f"invalid Ring VRF proof length: Ring VRF proof must be exactly {expected_len} bytes, got {len(proof)}") - if not skip_pedersen: - pedersen_len = PedersenVRF[cls.cv].proof_len() - pedersen_proof = PedersenVRF[cls.cv].decode(proof[:pedersen_len]) - offset = pedersen_len - else: - pedersen_proof = None - offset = 0 + pedersen_len = PedersenVRF[cls.cv].proof_len() + pedersen_proof = PedersenVRF[cls.cv].decode(proof[:pedersen_len]) + offset = pedersen_len params = RingProofParams(cv=cls.cv) expected = offset + RingProofPayload.encoded_len(params) @@ -137,7 +111,7 @@ def _payload(self) -> RingProofPayload: open_l_zeta_omega=self.open_l_zeta_omega, ) - def ring_proof_tuple(self) -> RingProofFields: + def as_ring_proof(self) -> RingProofFields: """Return the ring-proof fields in the order expected by the SNARK verifier.""" return RingProofFields( self.c_b.commitment, @@ -157,11 +131,6 @@ def ring_proof_tuple(self) -> RingProofFields: self.open_l_zeta_omega, ) - def require_pedersen_proof(self) -> PedersenVRF: - if self.pedersen_proof is None: - raise ValueError("Pedersen proof is missing") - return self.pedersen_proof - def _ring_proof_verifier( self, message: bytes | CurvePoint, @@ -181,7 +150,7 @@ def _ring_proof_verifier( witness_commitments, quotient_commitment = _proof_transcript_commitments(self, ring.params) return Verify( - self.ring_proof_tuple(), + self.as_ring_proof(), fixed_cols_cmts, rltn, res_plus_seed, @@ -204,9 +173,7 @@ def verify_ring_proof( ring: Ring, ring_root: RingRoot, ) -> bool: - """ - Verifies the Ring Proof - """ + """Spec section 5.2 step 2: verify the ring proof against `Y_bar`.""" if not ring_root.matches_ring(ring): return False return self._ring_proof_verifier(message, ring, ring_root).is_valid() @@ -221,24 +188,7 @@ def prove( ring: Ring, ring_root: RingRoot | None = None, ) -> RingVRF: - """ - Generate ring VRF proof (pedersen vrf proof + ring_proof). - - Args: - alpha: VRF input - ad: Additional data - secret_key: Prover's secret key - producer_key: Prover's public key - ring: Ring object containing member keys. Params are auto-constructed if not provided to Ring. - ring_root: Pre-computed ring root. If provided, skips expensive ring column construction (~335ms for 1023 members). - - Returns: - RingVRF proof - - Examples: - >>> ring = Ring(keys) # Automatically determines optimal domain size - >>> proof = RingVRF[Bandersnatch].prove(alpha, ad, sk, pk, ring) - """ + """Spec section 5.1: run Pedersen prove, then prove `Y_bar = producer_key + b*B` is in the ring.""" if producer_key != cls.cv.public_key_from_secret(secret_key): raise ValueError("producer_key does not match secret_key") @@ -267,10 +217,8 @@ def parse_keys(cls, keys: bytes) -> list[bytes]: return _parse_concatenated_keys(keys, cls.cv) def verify(self, input: bytes, ad_data: bytes, ring: Ring, ring_root: RingRoot) -> bool: - """ - Verify ring VRF proof (pedersen_proof + ring_proof) - """ - pedersen_proof = self.require_pedersen_proof() + """Spec section 5.2: Pedersen verify first, then ring-verify the blinded key.""" + pedersen_proof = self.pedersen_proof p_proof_valid = pedersen_proof.verify(input, ad_data) ring_proof_valid = self.verify_ring_proof(pedersen_proof.blinded_pk, ring, ring_root) diff --git a/dot_ring/vrf/transcript.py b/dot_ring/vrf/transcript.py index e3f2b2a..65dc0d7 100644 --- a/dot_ring/vrf/transcript.py +++ b/dot_ring/vrf/transcript.py @@ -1,20 +1,25 @@ +"""VRF transcript primitives from Bandersnatch VRF spec sections 1.5-1.9. + +The public helpers intentionally keep spec names: `vrf_transcript`, `nonce`, +`challenge`, and `point_to_hash`. +""" + from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any from dot_ring.curve.curve import CurveVariant from dot_ring.curve.point import CurvePoint from dot_ring.vrf.domain import DomSep from dot_ring.vrf.io import VrfIo -if TYPE_CHECKING: - from dot_ring.vrf.delinearize import DelinearizeScalars - SECURITY_PARAMETER = 128 CHALLENGE_LEN = SECURITY_PARAMETER // 8 class VrfTranscript: + """Append-only transcript with counter-mode XOF squeezing; no absorb after squeeze.""" + def __init__(self, label: bytes, hash_fn: Any) -> None: self._hash_fn = hash_fn self._absorbed = bytearray(label) @@ -81,6 +86,7 @@ def _challenge_scalar(curve: CurveVariant, transcript: VrfTranscript) -> int: def nonce(curve: CurveVariant, secret_scalar: int, transcript: VrfTranscript | None = None) -> int: + """Spec section 1.8: derive the nonce from `NonceExpand` then `Nonce`; abort on zero.""" t = transcript.copy() if transcript is not None else _transcript_for(curve) t_exp = t.copy() @@ -90,10 +96,14 @@ def nonce(curve: CurveVariant, secret_scalar: int, transcript: VrfTranscript | N t.absorb(bytes([DomSep.NONCE])) t.absorb(secret_hash) - return _nonce_scalar(curve, t) + nonce_scalar = _nonce_scalar(curve, t) + if nonce_scalar == 0: + raise ValueError("nonce scalar is zero") + return nonce_scalar def challenge(curve: CurveVariant, points: list[CurvePoint], transcript: VrfTranscript | None = None) -> int: + """Spec section 1.9: absorb `Challenge` and encoded points, then squeeze challenge_len.""" t = transcript.copy() if transcript is not None else _transcript_for(curve) t.absorb(bytes([DomSep.CHALLENGE])) for point in points: @@ -102,6 +112,7 @@ def challenge(curve: CurveVariant, points: list[CurvePoint], transcript: VrfTran def point_to_hash(curve: CurveVariant, point: CurvePoint, size: int = 32) -> bytes: + """Spec section 1.4: domain-separate `O` before producing the user-visible VRF hash.""" t = _transcript_for(curve) t.absorb(bytes([DomSep.POINT_TO_HASH])) t.absorb(point.point_to_string()) @@ -114,14 +125,14 @@ def vrf_transcript( ios: list[VrfIo], ad: bytes, ) -> tuple[VrfTranscript, VrfIo]: - t, scalars = vrf_transcript_scalars(curve, scheme, ios, ad) + """Spec section 1.7: bind scheme/I/O/AD and return the delinearized merged pair.""" + t, zs = vrf_transcript_scalars(curve, scheme, ios, ad) if not ios: zero = curve.identity() return t, VrfIo(zero, zero) if len(ios) == 1: return t, ios[0] - zs = [scalars.next() for _ in ios] input_acc = curve.msm([io.input for io in ios], zs) output_acc = curve.msm([io.output for io in ios], zs) return t, VrfIo(input_acc, output_acc) @@ -132,9 +143,8 @@ def vrf_transcript_scalars( scheme: DomSep, ios: list[VrfIo], ad: bytes, -) -> tuple[VrfTranscript, DelinearizeScalars]: - from dot_ring.vrf.delinearize import DelinearizeScalars - +) -> tuple[VrfTranscript, list[int]]: + """Spec section 1.7 plus section 1.6 delinearization scalars for these I/O pairs.""" t = _transcript_for(curve) t.absorb(bytes([scheme])) t.absorb(len(ios).to_bytes(8, "little")) @@ -142,7 +152,21 @@ def vrf_transcript_scalars( t.absorb(io.encode()) t.absorb(len(ad).to_bytes(8, "little")) t.absorb(ad) - return t, DelinearizeScalars(curve, t) + return t, delinearization_scalars(curve, t, len(ios)) + + +def delinearization_scalars(curve: CurveVariant, transcript: VrfTranscript, count: int) -> list[int]: + """Spec section 1.6: fork, absorb `Delinearize`, return `z_0 = 1` then squeezed scalars.""" + if count <= 0: + return [] + + t = transcript.copy() + t.absorb(bytes([DomSep.DELINEARIZE])) + order = curve.curve.params.subgroup_order + scalars = [1] + for _ in range(count - 1): + scalars.append(int.from_bytes(t.squeeze(CHALLENGE_LEN), "little") % order) + return scalars def schnorr_ios(curve: CurveVariant, public_key: CurvePoint, ios: list[VrfIo]) -> list[VrfIo]: diff --git a/dot_ring/vrf/vrf.py b/dot_ring/vrf/vrf.py index 0d2acc1..384f65d 100644 --- a/dot_ring/vrf/vrf.py +++ b/dot_ring/vrf/vrf.py @@ -1,27 +1,44 @@ from __future__ import annotations +from functools import lru_cache from typing import Any, ClassVar, Generic, Self, TypeVar from ..curve.curve import CurveVariant +from ..curve.point import CurvePoint +from ..curve.twisted_edwards.te_affine_point import TEAffinePoint C = TypeVar("C", bound=CurveVariant) -class VRF(Generic[C]): - """ - Base VRF (Verifiable Random Function) implementation. +@lru_cache(maxsize=32) +def _cofactor_inverse(cofactor: int, subgroup_order: int) -> int: + return pow(cofactor, -1, subgroup_order) - This class provides the core functionality for VRF operations, - following the shared dot-ring VRF interface. - Usage with subscript syntax: - >>> from dot_ring.curve.specs.bandersnatch import Bandersnatch - >>> from dot_ring.vrf.ietf import TinyVRF - >>> proof = TinyVRF[Bandersnatch].prove(alpha, secret_key, additional_data) - """ +class VRF(Generic[C]): + """Base VRF class that specializes implementations by curve variant.""" cv: ClassVar[CurveVariant] + @staticmethod + def _valid_point(point: CurvePoint) -> bool: + """Verifier-side `dec_point`: nonidentity and in the prime-order subgroup.""" + if point.is_identity(): + return False + + cofactor = point.curve.params.cofactor + if cofactor == 1: + return True + + cofactor_inv = _cofactor_inverse(cofactor, point.curve.params.subgroup_order) + if isinstance(point, TEAffinePoint): + cleared = TEAffinePoint.__mul__(point, cofactor) + else: + cleared = point * cofactor + if cleared.is_identity(): + return False + return cleared * cofactor_inv == point + def __class_getitem__(cls, curve_variant: CurveVariant | Any) -> type[Self] | Any: """ Create a specialized VRF class for a specific curve variant. diff --git a/tests/test_ark_vrf.py b/tests/test_ark_vrf.py index 077e924..e053d1e 100644 --- a/tests/test_ark_vrf.py +++ b/tests/test_ark_vrf.py @@ -315,8 +315,6 @@ def test_negative_and_malformed_proofs() -> None: invalid_kzg_point = proof_bytes[:pedersen_len] + b"\xff" * 48 + proof_bytes[pedersen_len + 48 :] with pytest.raises(ValueError): RingVRF[Bandersnatch].decode(invalid_kzg_point) - with pytest.raises(ValueError, match="invalid Ring VRF proof length"): - RingVRF[Bandersnatch].decode(bytes.fromhex(ring_vector["ring_proof"])[:-1], skip_pedersen=True) with pytest.raises(ValueError, match="invalid ring root length"): RingRoot.decode(ring_root.encode()[:-1]) with pytest.raises(ValueError): diff --git a/tests/test_coverage/test_columns.py b/tests/test_coverage/test_columns.py index ed8eb31..24d37b2 100644 --- a/tests/test_coverage/test_columns.py +++ b/tests/test_coverage/test_columns.py @@ -2,15 +2,15 @@ from dot_ring.curve.specs.bandersnatch import Bandersnatch from dot_ring.ring_proof.columns.columns import Column, WitnessColumnBuilder -from dot_ring.ring_proof.constants import DEFAULT_SIZE, OMEGAS, S_PRIME from dot_ring.ring_proof.params import RingProofParams from dot_ring.vrf.ring import Ring def test_column_interpolate_rejects_oversize_evals(): + params = RingProofParams() col = Column("x", [1, 2, 3], size=2) with pytest.raises(ValueError, match="exceeds column size"): - col.interpolate(domain_omega=OMEGAS[DEFAULT_SIZE], prime=S_PRIME) + col.interpolate(domain_omega=params.omega, prime=params.prime) def test_column_commit_requires_coeffs(): diff --git a/tests/test_coverage/test_fft.py b/tests/test_coverage/test_fft.py index 02fe218..90191ff 100644 --- a/tests/test_coverage/test_fft.py +++ b/tests/test_coverage/test_fft.py @@ -1,7 +1,6 @@ """Tests for FFT module to improve coverage.""" -from dot_ring.ring_proof.constants import OMEGA_512 as OMEGA -from dot_ring.ring_proof.constants import S_PRIME +from dot_ring.ring_proof.params import RingProofParams from dot_ring.ring_proof.polynomial.fft import ( _fft_in_place, _get_bit_reverse, @@ -14,6 +13,10 @@ class TestFFT: """Test cases for FFT functions.""" + PARAMS = RingProofParams() + PRIME = PARAMS.prime + OMEGA = PARAMS.omega + # Test constants - using a smaller prime for easier testing SMALL_PRIME = 17 SMALL_OMEGA = 3 # primitive 4th root of unity mod 17 (3^4 = 81 = 4*17 + 13... let's use a proper one) @@ -81,10 +84,10 @@ def test_fft_in_place_size_2(self): # omega^2 = 1, omega != 1, so omega = -1 = p-1 = 16 mod 17 # Just test that FFT runs without error for small inputs # Use larger prime field for proper FFT - omega_2 = pow(OMEGA, 512 // 2, S_PRIME) # Scale down from 512-th root + omega_2 = pow(self.OMEGA, 512 // 2, self.PRIME) # Scale down from 512-th root coeffs = [3, 5] - _fft_in_place(coeffs, omega_2, S_PRIME) + _fft_in_place(coeffs, omega_2, self.PRIME) # Just verify the result has same length assert len(coeffs) == 2 @@ -93,16 +96,16 @@ def test_inverse_fft(self): # Use the actual module constants original = [1, 2, 3, 4, 5, 6, 7, 8] - # Need to find proper omega for size 8 in S_PRIME + # Need to find proper omega for size 8 in the Bandersnatch field. # Using module omega scaled down - omega_8 = pow(OMEGA, 512 // 8, S_PRIME) # Scale down from 512-th root + omega_8 = pow(self.OMEGA, 512 // 8, self.PRIME) # Scale down from 512-th root # Compute FFT values = original[:] - _fft_in_place(values, omega_8, S_PRIME) + _fft_in_place(values, omega_8, self.PRIME) # Compute inverse FFT - recovered = inverse_fft(values, omega_8, S_PRIME) + recovered = inverse_fft(values, omega_8, self.PRIME) assert recovered == original @@ -157,16 +160,16 @@ def test_fft_roundtrip_with_real_constants(self): """Test FFT/IFFT roundtrip with actual module constants.""" # Use actual constants from the module n = 8 - omega_n = pow(OMEGA, 512 // n, S_PRIME) + omega_n = pow(self.OMEGA, 512 // n, self.PRIME) original = [i * 12345 for i in range(n)] # Some test values # Forward FFT values = original[:] - _fft_in_place(values, omega_n, S_PRIME) + _fft_in_place(values, omega_n, self.PRIME) # Inverse FFT - recovered = inverse_fft(values, omega_n, S_PRIME) + recovered = inverse_fft(values, omega_n, self.PRIME) assert recovered == original @@ -183,8 +186,8 @@ def test_caching_bit_reverse(self): def test_caching_twiddle_factors(self): """Test that twiddle factor caching works correctly.""" - result1 = _get_twiddle_factors(8, OMEGA, S_PRIME) - result2 = _get_twiddle_factors(8, OMEGA, S_PRIME) + result1 = _get_twiddle_factors(8, self.OMEGA, self.PRIME) + result2 = _get_twiddle_factors(8, self.OMEGA, self.PRIME) assert result1 == result2 assert result1 is result2 # Same cached object diff --git a/tests/test_coverage/test_ops.py b/tests/test_coverage/test_ops.py index e9348df..be608ed 100644 --- a/tests/test_coverage/test_ops.py +++ b/tests/test_coverage/test_ops.py @@ -1,6 +1,6 @@ """Tests for polynomial operations module to improve coverage.""" -from dot_ring.ring_proof.constants import D_512, D_2048, S_PRIME +from dot_ring.ring_proof.params import RingProofParams from dot_ring.ring_proof.polynomial.ops import ( get_root_of_unity, lagrange_basis_polynomial, @@ -17,6 +17,11 @@ class TestPolynomialOps: """Test cases for polynomial operations.""" PRIME = 17 # Small prime for testing + PARAMS_512 = RingProofParams(domain_size=512, max_ring_size=1) + PARAMS_2048 = RingProofParams(domain_size=2048, max_ring_size=1) + RING_PRIME = PARAMS_512.prime + DOMAIN_512 = PARAMS_512.domain + DOMAIN_2048 = PARAMS_2048.domain def test_poly_add_same_length(self): """Test polynomial addition with same length.""" @@ -53,7 +58,7 @@ def test_poly_multiply_larger_fft(self): p1 = list(range(1, 65)) # 64 coefficients p2 = list(range(1, 65)) - result = poly_multiply(p1, p2, S_PRIME) + result = poly_multiply(p1, p2, self.RING_PRIME) # Result should have len(p1) + len(p2) - 1 = 127 coefficients assert len(result) == 127 @@ -170,46 +175,46 @@ def test_lagrange_basis_polynomial_basic(self): assert val_at_2 == 0 def test_lagrange_basis_polynomial_d512(self): - """Test Lagrange basis polynomial for roots of unity domain D_512.""" - # Should use optimized path for D_512 - basis = lagrange_basis_polynomial(D_512, 0, S_PRIME) + """Test Lagrange basis polynomial for roots of unity domain DOMAIN_512.""" + # Should use optimized path for DOMAIN_512 + basis = lagrange_basis_polynomial(self.DOMAIN_512, 0, self.RING_PRIME) assert len(basis) == 512 def test_lagrange_basis_polynomial_d2048(self): - """Test Lagrange basis polynomial for roots of unity domain D_2048.""" - # Should use optimized path for D_2048 - basis = lagrange_basis_polynomial(D_2048, 0, S_PRIME) + """Test Lagrange basis polynomial for roots of unity domain DOMAIN_2048.""" + # Should use optimized path for DOMAIN_2048 + basis = lagrange_basis_polynomial(self.DOMAIN_2048, 0, self.RING_PRIME) assert len(basis) == 2048 def test_lagrange_basis_polynomial_generic_root_domain(self): """Test optimized Lagrange basis on generated roots-of-unity domains.""" - omega = get_root_of_unity(8, S_PRIME) - domain = [pow(omega, i, S_PRIME) for i in range(8)] - basis = lagrange_basis_polynomial(domain, 3, S_PRIME) + omega = get_root_of_unity(8, self.RING_PRIME) + domain = [pow(omega, i, self.RING_PRIME) for i in range(8)] + basis = lagrange_basis_polynomial(domain, 3, self.RING_PRIME) for index, point in enumerate(domain): expected = 1 if index == 3 else 0 - assert poly_evaluate_single(basis, point, S_PRIME) == expected + assert poly_evaluate_single(basis, point, self.RING_PRIME) == expected def test_get_root_of_unity_caching(self): """Test that root of unity computation is cached.""" # First call - root1 = get_root_of_unity(8, S_PRIME) + root1 = get_root_of_unity(8, self.RING_PRIME) # Second call should be cached - root2 = get_root_of_unity(8, S_PRIME) + root2 = get_root_of_unity(8, self.RING_PRIME) assert root1 == root2 # Verify it's actually a root of unity - assert pow(root1, 8, S_PRIME) == 1 + assert pow(root1, 8, self.RING_PRIME) == 1 def test_get_root_of_unity_various_sizes(self): """Test root of unity for various sizes.""" for n in [4, 8, 16, 32, 64]: - root = get_root_of_unity(n, S_PRIME) - assert pow(root, n, S_PRIME) == 1 + root = get_root_of_unity(n, self.RING_PRIME) + assert pow(root, n, self.RING_PRIME) == 1 # Should be primitive (root^(n/2) != 1) if n > 1: - assert pow(root, n // 2, S_PRIME) != 1 + assert pow(root, n // 2, self.RING_PRIME) != 1 class TestPolynomialOpsEdgeCases: diff --git a/tests/test_coverage/test_params.py b/tests/test_coverage/test_params.py index 22f3ff1..474aa40 100644 --- a/tests/test_coverage/test_params.py +++ b/tests/test_coverage/test_params.py @@ -1,82 +1,26 @@ import pytest from dot_ring.curve.specs.baby_jubjub import BabyJubJub -from dot_ring.ring_proof.constants import D_2048, DEFAULT_SIZE, MAX_RING_SIZE, OMEGA_2048, S_PRIME -from dot_ring.ring_proof.params import ( - RingProofParams, - _domain_for_size, - _extend_root_to_size, - _is_power_of_two, - _omega_for_domain, - _sqrt_mod_prime, -) - - -def test_is_power_of_two(): - assert _is_power_of_two(1) - assert _is_power_of_two(8) - assert not _is_power_of_two(0) - assert not _is_power_of_two(6) - - -def test_omega_for_domain_valid(): - omega = _omega_for_domain(DEFAULT_SIZE, S_PRIME, OMEGA_2048, 2048) - assert omega == pow(OMEGA_2048, 2048 // DEFAULT_SIZE, S_PRIME) - - -def test_omega_for_domain_invalid(): - with pytest.raises(ValueError, match="must divide"): - _omega_for_domain(3, S_PRIME, OMEGA_2048, 2048) - - -def test_domain_for_size_matches_omega(): - domain = _domain_for_size(8, S_PRIME, OMEGA_2048, 2048) - omega = _omega_for_domain(8, S_PRIME, OMEGA_2048, 2048) - assert len(domain) == 8 - assert domain[0] == 1 - assert domain[1] == omega - - -def test_sqrt_mod_prime_zero(): - assert _sqrt_mod_prime(0, 7) == 0 - - -def test_sqrt_mod_prime_mod4_3(): - root = _sqrt_mod_prime(2, 7) - assert (root * root) % 7 == 2 - - -def test_sqrt_mod_prime_tonelli_shanks(): - root = _sqrt_mod_prime(10, 13) - assert (root * root) % 13 == 10 - - -def test_sqrt_mod_prime_non_residue(): - with pytest.raises(ValueError, match="No square root"): - _sqrt_mod_prime(2, 13) - - -def test_extend_root_to_size_grows(): - root, size = _extend_root_to_size(1, 4, 8, 7) - assert size == 8 - assert root == 1 +from dot_ring.ring_proof.params import RingProofParams def test_ring_proof_params_defaults(): params = RingProofParams() - assert params.domain_size == DEFAULT_SIZE - assert params.max_ring_size == MAX_RING_SIZE - assert params.radix_domain_size == DEFAULT_SIZE * 4 - assert params.omega == pow(OMEGA_2048, 2048 // DEFAULT_SIZE, S_PRIME) - assert params.radix_domain == list(D_2048) + assert params.domain_size == 512 + assert params.max_ring_size == 255 + assert params.radix_domain_size == 2048 + assert pow(params.omega, params.domain_size, params.prime) == 1 + assert pow(params.omega, params.domain_size // 2, params.prime) != 1 + assert len(params.domain) == params.domain_size + assert len(params.radix_domain) == params.radix_domain_size assert params.radix_shift == 4 assert params.last_index == params.domain_size - params.padding_rows assert params.max_effective_ring_size == params.domain_size - params.scalar_bits - params.padding_rows def test_baby_jubjub_ring_proof_params_are_unsupported(): - with pytest.raises(ValueError, match="BabyJubJub ring proofs require field modulus"): + with pytest.raises(ValueError, match="BabyJubJub ring proofs require a primitive"): RingProofParams(cv=BabyJubJub) diff --git a/tests/test_coverage/test_pcs_utils.py b/tests/test_coverage/test_pcs_utils.py index cabd1cd..2d457c8 100644 --- a/tests/test_coverage/test_pcs_utils.py +++ b/tests/test_coverage/test_pcs_utils.py @@ -1,8 +1,10 @@ """Additional tests for utils module to improve coverage.""" -from dot_ring.ring_proof.constants import S_PRIME +from dot_ring.curve.specs.bandersnatch import Bandersnatch from dot_ring.ring_proof.pcs.utils import synthetic_div_with_eval +RING_PRIME = Bandersnatch.curve.params.field_modulus + class TestPCSUtils: """Test PCS utility functions.""" @@ -10,7 +12,7 @@ class TestPCSUtils: def test_synthetic_div_with_eval_matches_checked_division(self): poly = [3, 5, 7, 11, 13] x = 17 - y = sum(c * pow(x, i, S_PRIME) for i, c in enumerate(poly)) % S_PRIME + y = sum(c * pow(x, i, RING_PRIME) for i, c in enumerate(poly)) % RING_PRIME quotient, value = synthetic_div_with_eval(poly, x) diff --git a/tests/test_curve_ops/test_gaps.py b/tests/test_curve_ops/test_gaps.py index 636d546..a36eff8 100644 --- a/tests/test_curve_ops/test_gaps.py +++ b/tests/test_curve_ops/test_gaps.py @@ -168,51 +168,6 @@ def test_uncompressed_curve_decode(self): except Exception as e: pytest.skip(f"Ed448 not fully supported or failed: {e}") - def test_ring_decode_skip_pedersen(self): - """Test RingVRF.decode with skip_pedersen=True.""" - from dot_ring.vrf.ring import RingVRF - - # Generate a valid proof - alpha = b"test" - ad = b"ad" - sk = b"secret" - pk = Bandersnatch.public_key_from_secret(sk) - keys = [pk] - - params = RingProofParams() - ring = Ring(keys, params) - ring_root = RingRoot.from_ring(ring, params) - proof = RingVRF[Bandersnatch].prove(alpha, ad, sk, pk, ring, ring_root) - proof_bytes = proof.encode() - - # skip_pedersen=True expects bytes that start at the ring proof payload. - ring_proof_bytes = proof_bytes[192:] - parsed = RingVRF[Bandersnatch].decode(ring_proof_bytes, skip_pedersen=True) - assert parsed.pedersen_proof is None - - def test_ring_verify_missing_pedersen(self): - """Test RingVRF.verify raises ValueError if pedersen_proof is missing.""" - from dot_ring.vrf.ring import RingVRF - - # Create a dummy RingVRF with pedersen_proof=None - # We can use the one parsed above - alpha = b"test" - ad = b"ad" - sk = b"secret" - pk = Bandersnatch.public_key_from_secret(sk) - keys = [pk] - - params = RingProofParams() - ring = Ring(keys, params) - ring_root = RingRoot.from_ring(ring, params) - proof = RingVRF[Bandersnatch].prove(alpha, ad, sk, pk, ring, ring_root) - proof_bytes = proof.encode() - ring_proof_bytes = proof_bytes[192:] - parsed = RingVRF[Bandersnatch].decode(ring_proof_bytes, skip_pedersen=True) - - with pytest.raises(ValueError, match="Pedersen proof is missing"): - parsed.verify(alpha, ad, ring, ring_root) - def test_ring_handles_invalid_keys(self): """Test Ring construction with invalid keys.""" # Invalid key string diff --git a/tests/test_ring_vrf/test_audit_regressions.py b/tests/test_ring_vrf/test_audit_regressions.py index 0e14986..c4fb761 100644 --- a/tests/test_ring_vrf/test_audit_regressions.py +++ b/tests/test_ring_vrf/test_audit_regressions.py @@ -1,7 +1,6 @@ import pytest from dot_ring import Bandersnatch, RingVRF -from dot_ring.ring_proof.constants import PaddingPoint from dot_ring.ring_proof.params import RingProofParams from dot_ring.vrf.pedersen import PedersenVRF from dot_ring.vrf.ring import Ring, RingContext, RingRoot @@ -49,11 +48,14 @@ def test_ring_root_match_rejects_mismatched_ring(): def test_ring_keys_pad_decode_failures_in_place(): pk1, pk2 = _keys(2) identity = (1).to_bytes(32, "little") - ring = Ring([pk1, b"", identity, pk2], RingProofParams(test_vectors=True)) + params = RingProofParams(test_vectors=True) + ring = Ring([pk1, b"", identity, pk2], params) + padding_point = params.cv.curve.params.auxiliary_points.padding_point + assert padding_point is not None assert ring.nm_points[0] == _point_tuple(pk1) - assert ring.nm_points[1] == PaddingPoint - assert ring.nm_points[2] == PaddingPoint + assert ring.nm_points[1] == padding_point + assert ring.nm_points[2] == padding_point assert ring.nm_points[3] == _point_tuple(pk2) diff --git a/tests/test_verify_ring_sig.py b/tests/test_verify_ring_sig.py index 5c1e58b..718f50c 100644 --- a/tests/test_verify_ring_sig.py +++ b/tests/test_verify_ring_sig.py @@ -8,11 +8,11 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) from dot_ring import Bandersnatch, blst -from dot_ring.ring_proof.constants import EVAL_DOMAINS, S_PRIME +from dot_ring.ring_proof.params import RingProofParams from dot_ring.ring_proof.pcs.kzg import KZG from dot_ring.ring_proof.pcs.utils import g2_to_blst from dot_ring.ring_proof.transcript.transcript import FiatShamirTranscript -from dot_ring.ring_proof.verify import Verify +from dot_ring.ring_proof.verify import RingProofFields, Verify from tests.utils.rust_serde import ( compressed_g1_to_uncompressed_bytes, compressed_g2_to_uncompressed_bytes, @@ -23,7 +23,7 @@ ) -def parse_proof_from_json(proof_json: dict) -> tuple: +def parse_proof_from_json(proof_json: dict) -> tuple[RingProofFields, dict]: proof = proof_json["proof"] col_cmts_bytes = bytes.fromhex(proof["column_commitments"]) @@ -48,22 +48,22 @@ def parse_proof_from_json(proof_json: dict) -> tuple: phi_zeta = deserialize_bls12_381_g1(bytes.fromhex(proof["agg_at_zeta_proof"])) phi_zeta_omega = deserialize_bls12_381_g1(bytes.fromhex(proof["lin_at_zeta_omega_proof"])) - proof_tuple = ( - c_b, - c_accip, - c_accx, - c_accy, - px_zeta, - py_zeta, - s_zeta, - b_zeta, - accip_zeta, - accx_zeta, - accy_zeta, - c_q, - l_zeta_omega, - phi_zeta, - phi_zeta_omega, + proof_fields = RingProofFields( + c_b=c_b, + c_accip=c_accip, + c_accx=c_accx, + c_accy=c_accy, + px_zeta=px_zeta, + py_zeta=py_zeta, + s_zeta=s_zeta, + b_zeta=b_zeta, + accip_zeta=accip_zeta, + accx_zeta=accx_zeta, + accy_zeta=accy_zeta, + c_q=c_q, + l_zeta_omega=l_zeta_omega, + phi_zeta=phi_zeta, + phi_zeta_omega=phi_zeta_omega, ) raw_bytes = { @@ -76,7 +76,7 @@ def parse_proof_from_json(proof_json: dict) -> tuple: "quotient_commitment": cq_bytes, } - return proof_tuple, raw_bytes + return proof_fields, raw_bytes def parse_verifier_key(vk_hex: str) -> tuple[bytes, list]: @@ -93,23 +93,25 @@ def verify_vector(vector_path: Path) -> None: with open(vector_path) as f: proof_data = json.load(f) - params = proof_data["metadata"]["parameters"] - domain = EVAL_DOMAINS[params["domain_size"]] - padding_rows = params.get("padding_rows", 4) + vector_params = proof_data["metadata"]["parameters"] + ring_params = RingProofParams(domain_size=vector_params["domain_size"], max_ring_size=1) + prime = ring_params.prime + domain = ring_params.domain + padding_rows = vector_params.get("padding_rows", ring_params.padding_rows) - seed_x_bytes = bytes.fromhex(params["seed"]["x"]) - seed_y_bytes = bytes.fromhex(params["seed"]["y"]) + seed_x_bytes = bytes.fromhex(vector_params["seed"]["x"]) + seed_y_bytes = bytes.fromhex(vector_params["seed"]["y"]) seed_point = deserialize_bandersnatch_point(seed_x_bytes, seed_y_bytes) - result_x_bytes = bytes.fromhex(params["result"]["x"]) - result_y_bytes = bytes.fromhex(params["result"]["y"]) + result_x_bytes = bytes.fromhex(vector_params["result"]["x"]) + result_y_bytes = bytes.fromhex(vector_params["result"]["y"]) result_point = deserialize_bandersnatch_point(result_x_bytes, result_y_bytes) result_point = Bandersnatch.point(result_point) seed_point = Bandersnatch.point(seed_point) result_plus_seed = result_point + seed_point - proof_tuple, raw_bytes = parse_proof_from_json(proof_data) + proof_fields, raw_bytes = parse_proof_from_json(proof_data) vk_bytes, fixed_cols = parse_verifier_key(proof_data["verifier_key"]["verification_key"]) # Update SRS from verifier key @@ -142,11 +144,11 @@ def verify_vector(vector_path: Path) -> None: quotient_compressed = bytes.fromhex(proof_data["proof"]["quotient_commitment"]) quotient_uncompressed = compressed_g1_to_uncompressed_bytes(quotient_compressed) - transcript_prefix = FiatShamirTranscript(S_PRIME, b"w3f-ring-proof-test") + transcript_prefix = FiatShamirTranscript(prime, b"w3f-ring-proof-test") transcript_prefix.absorb_labeled(b"vk", vk_uncompressed) verifier = Verify( - proof=proof_tuple, + proof=proof_fields, fixed_cols=fixed_cols, relation_to_prove=result_point, result_plus_seed=result_plus_seed, From e7b5cef1a4c1865233933aa38cae54a9a3ab9580 Mon Sep 17 00:00:00 2001 From: Prasad Kumkar Date: Tue, 23 Jun 2026 02:04:45 +0530 Subject: [PATCH 3/4] refactor: VRF and Ring Proof impl --- .gitignore | 2 +- README.md | 9 +- dot_ring/__init__.py | 61 +--- dot_ring/curve/curve.py | 28 +- dot_ring/curve/glv.py | 22 +- dot_ring/curve/montgomery/mg_affine_point.py | 88 ++--- dot_ring/curve/montgomery/mg_curve.py | 55 +-- dot_ring/curve/point.py | 82 +++-- .../short_weierstrass/sw_affine_point.py | 94 +++-- dot_ring/curve/specs/baby_jubjub.py | 11 +- dot_ring/curve/specs/bandersnatch.py | 23 +- dot_ring/curve/specs/bandersnatch_sw.py | 17 +- dot_ring/curve/specs/bls12_381_G1.py | 20 +- dot_ring/curve/specs/bls12_381_G2.py | 109 +++--- dot_ring/curve/specs/curve25519.py | 20 +- dot_ring/curve/specs/curve448.py | 20 +- dot_ring/curve/specs/ed25519.py | 43 ++- dot_ring/curve/specs/ed448.py | 37 +- dot_ring/curve/specs/jubjub.py | 11 +- dot_ring/curve/specs/p256.py | 54 ++- dot_ring/curve/specs/p384.py | 20 +- dot_ring/curve/specs/p521.py | 20 +- dot_ring/curve/specs/secp256k1.py | 22 +- .../curve/twisted_edwards/te_affine_point.py | 76 ++-- .../twisted_edwards/te_projective_point.py | 4 +- dot_ring/ring_proof/columns/columns.py | 6 +- dot_ring/ring_proof/polynomial/fft.py | 5 +- .../{vrf/ring => ring_proof}/proof_builder.py | 21 +- .../{vrf/ring => ring_proof}/proof_payload.py | 1 + dot_ring/ring_proof/ring_curve.py | 9 +- dot_ring/scripts/export_python_proof.py | 337 ------------------ dot_ring/vrf/codec.py | 77 ++++ dot_ring/vrf/ietf/__init__.py | 4 +- dot_ring/vrf/ietf/thin.py | 166 ++++----- dot_ring/vrf/ietf/tiny.py | 120 ++----- dot_ring/vrf/io.py | 14 - dot_ring/vrf/pedersen/__init__.py | 3 +- dot_ring/vrf/pedersen/batch_verifier.py | 332 ----------------- dot_ring/vrf/pedersen/vrf.py | 310 +++++++++++----- dot_ring/vrf/primitives.py | 174 +++++++++ dot_ring/vrf/ring/__init__.py | 5 - dot_ring/vrf/ring/batch_verifier.py | 262 -------------- dot_ring/vrf/ring/context.py | 70 ---- dot_ring/vrf/ring/members.py | 9 +- dot_ring/vrf/ring/root.py | 31 +- dot_ring/vrf/ring/vrf.py | 104 +++++- dot_ring/vrf/transcript.py | 238 ------------- dot_ring/vrf/vrf.py | 45 +-- scripts/generate_test_vectors.py | 20 +- tests/benchmark/bench_ring_proof.py | 47 +-- tests/test_ark_vrf.py | 164 +++------ tests/test_bandersnatch_ark.py | 8 +- tests/test_coverage/test_glv.py | 12 +- tests/test_curve_ops/test_curve.py | 87 +++-- tests/test_curve_ops/test_curve_ops.py | 6 +- tests/test_curve_ops/test_curves_s2p_p2s.py | 16 +- tests/test_curve_ops/test_gaps.py | 6 +- tests/test_curve_ops/test_glv.py | 4 +- tests/test_curve_ops/test_mg_curve.py | 8 +- tests/test_dot_ring_vectors.py | 2 +- .../test_bls12_381_G2_ssw_nu.py | 2 +- .../test_bls12_381_G2_ssw_ro.py | 2 +- .../test_h2c_suites/test_e2c_bandersnatch.py | 6 +- tests/test_h2c_suites/test_h2c_nu.py | 2 +- tests/test_h2c_suites/test_h2c_ro.py | 2 +- tests/test_ietf/test_ietf_ark.py | 4 +- tests/test_pedersen/test_pedersen_ark.py | 4 +- tests/test_pedersen/test_pedersen_base.py | 2 +- tests/test_ring_vrf/test_audit_regressions.py | 22 +- tests/test_vectors.py | 2 +- 70 files changed, 1366 insertions(+), 2353 deletions(-) rename dot_ring/{vrf/ring => ring_proof}/proof_builder.py (96%) rename dot_ring/{vrf/ring => ring_proof}/proof_payload.py (99%) delete mode 100644 dot_ring/scripts/export_python_proof.py create mode 100644 dot_ring/vrf/codec.py delete mode 100644 dot_ring/vrf/io.py delete mode 100644 dot_ring/vrf/pedersen/batch_verifier.py create mode 100644 dot_ring/vrf/primitives.py delete mode 100644 dot_ring/vrf/ring/batch_verifier.py delete mode 100644 dot_ring/vrf/ring/context.py delete mode 100644 dot_ring/vrf/transcript.py diff --git a/.gitignore b/.gitignore index 5d2d4f9..a7574b4 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,4 @@ perf *.html .coverage -*.ignore \ No newline at end of file +**/*.ignore \ No newline at end of file diff --git a/README.md b/README.md index bf61481..fed1bb7 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,8 @@ is_valid = proof.verify(alpha, ad) ### Ring VRF ```python -from dot_ring import Bandersnatch, RingContext, RingVRF +from dot_ring import Bandersnatch, Ring, RingRoot, RingVRF +from dot_ring.ring_proof.params import RingProofParams # Setup ring my_pk = Bandersnatch.public_key_from_secret(secret_key) @@ -105,9 +106,9 @@ other_pks = [ ] ring_pks = [my_pk, *other_pks] -context = RingContext.from_ring_size(len(ring_pks), cv=Bandersnatch) -ring = context.ring(ring_pks) -ring_root = context.ring_root(ring) +params = RingProofParams.from_ring_size(len(ring_pks), cv=Bandersnatch) +ring = Ring(ring_pks, params) +ring_root = RingRoot.from_ring(ring, params) # Generate proof proof = RingVRF[Bandersnatch].prove(alpha, ad, secret_key, my_pk, ring, ring_root) diff --git a/dot_ring/__init__.py b/dot_ring/__init__.py index f1c3e1d..bf4f68e 100644 --- a/dot_ring/__init__.py +++ b/dot_ring/__init__.py @@ -1,42 +1,8 @@ -""" -dot-ring: A Python library for Verifiable Random Functions with Additional Data (VRF-AD). +"""Public VRF, ring, and curve-suite exports.""" -Supports 10+ elliptic curves including Tiny, Thin, Pedersen, and Ring VRF schemes. - -Example usage: - >>> from dot_ring import Bandersnatch, RingContext, TinyVRF, PedersenVRF, RingVRF - >>> - >>> # Tiny VRF - >>> proof = TinyVRF[Bandersnatch].prove(alpha, secret_key, additional_data) - >>> is_valid = proof.verify(public_key, alpha, additional_data) - >>> - >>> # Pedersen VRF - >>> proof = PedersenVRF[Bandersnatch].prove(alpha, secret_key, additional_data) - >>> is_valid = proof.verify(alpha, additional_data) - >>> - >>> # Ring VRF - >>> context = RingContext.from_ring_size(len(keys), cv=Bandersnatch) - >>> ring = context.ring(keys) - >>> ring_root = context.ring_root(ring) - >>> proof = RingVRF[Bandersnatch].prove(alpha, ad, secret_key, producer_key, ring, ring_root) - >>> is_valid = proof.verify(alpha, ad, ring, ring_root) -""" - -__version__ = "0.1.0" - -# ============================================================================= -# VRF Implementations -# ============================================================================= -# ZK-friendly curves from dot_ring.curve.specs.baby_jubjub import BabyJubJub - -# ============================================================================= -# Curve Variants - Primary curves -# ============================================================================= from dot_ring.curve.specs.bandersnatch import Bandersnatch, Bandersnatch_SHAKE128 from dot_ring.curve.specs.bandersnatch_sw import Bandersnatch_SW - -# BLS12-381 curves from dot_ring.curve.specs.bls12_381_G1 import BLS12_381_G1_NU, BLS12_381_G1_RO from dot_ring.curve.specs.bls12_381_G2 import BLS12_381_G2_NU, BLS12_381_G2_RO from dot_ring.curve.specs.curve448 import Curve448_NU, Curve448_RO @@ -44,19 +10,14 @@ from dot_ring.curve.specs.ed448 import Ed448_NU, Ed448_RO from dot_ring.curve.specs.ed25519 import Ed25519_NU, Ed25519_RO, Ed25519_TAI from dot_ring.curve.specs.jubjub import JubJub - -# NIST curves from dot_ring.curve.specs.p256 import P256_NU, P256_RO, P256_TAI from dot_ring.curve.specs.p384 import P384_NU, P384_RO from dot_ring.curve.specs.p521 import P521_NU, P521_RO from dot_ring.curve.specs.secp256k1 import Secp256k1_NU, Secp256k1_RO -from dot_ring.vrf.ietf import ThinBatchVerifier, ThinVRF, TinyVRF -from dot_ring.vrf.pedersen import PedersenBatchVerifier, PedersenVRF -from dot_ring.vrf.ring import Ring, RingBatchVerifier, RingContext, RingRoot, RingRootBuilder, RingVRF +from dot_ring.vrf.ietf import ThinVRF, TinyVRF +from dot_ring.vrf.pedersen import PedersenVRF +from dot_ring.vrf.ring import Ring, RingRoot, RingVRF -# ============================================================================= -# Convenience aliases -# ============================================================================= Ed25519 = Ed25519_TAI Ed448 = Ed448_RO Curve25519 = Curve25519_RO @@ -68,23 +29,11 @@ BLS12_381_G1 = BLS12_381_G1_RO BLS12_381_G2 = BLS12_381_G2_RO -# ============================================================================= -# Public API -# ============================================================================= __all__ = [ - # Version - "__version__", - # VRF implementations "TinyVRF", "ThinVRF", - "ThinBatchVerifier", "PedersenVRF", - "PedersenBatchVerifier", "RingVRF", - "RingContext", - "RingRootBuilder", - "RingBatchVerifier", - # Primary curves "Bandersnatch", "Bandersnatch_SHAKE128", "Bandersnatch_SW", @@ -115,10 +64,8 @@ "Secp256k1", "Secp256k1_RO", "Secp256k1_NU", - # ZK-friendly curves "BabyJubJub", "JubJub", - # BLS12-381 "BLS12_381_G1", "BLS12_381_G1_RO", "BLS12_381_G1_NU", diff --git a/dot_ring/curve/curve.py b/dot_ring/curve/curve.py index d589673..ebc6f13 100644 --- a/dot_ring/curve/curve.py +++ b/dot_ring/curve/curve.py @@ -366,38 +366,20 @@ def point( return x if y is None: x, y = x - return self.point_type(x, y, self.curve) - - def identity(self) -> CurvePoint[Curve[CoordT], CoordT]: - return self.point_type.identity(self.curve) - - def generator_point(self) -> CurvePoint[Curve[CoordT], CoordT]: - return self.point_type.generator_point(self.curve) + return self.point_type(x, y) def public_key_from_secret(self, secret_key: bytes) -> bytes: if not isinstance(secret_key, bytes | bytearray): raise TypeError("secret_key must be bytes") secret_scalar = int.from_bytes(secret_key, "little") - return (self.generator_point() * secret_scalar).point_to_string() + return (self.point_type.generator_point() * secret_scalar).point_to_string() def secret_from_seed(self, seed: bytes) -> tuple[bytes, bytes]: if not isinstance(seed, bytes | bytearray): raise TypeError("seed must be bytes") - from dot_ring.vrf.transcript import scalar_encode, secret_from_seed_scalar + from dot_ring.vrf.codec import enc_scalar + from dot_ring.vrf.primitives import secret_from_seed_scalar secret_scalar = secret_from_seed_scalar(self, bytes(seed)) - secret_key = scalar_encode(self, secret_scalar) + secret_key = enc_scalar(self, secret_scalar) return self.public_key_from_secret(secret_key), secret_key - - def string_to_point(self, data: str | bytes) -> CurvePoint[Curve[CoordT], CoordT]: - return self.point_type.string_to_point(data, self.curve) - - def encode_to_curve(self, alpha_string: bytes | str, salt: bytes | str = b"") -> CurvePoint[Curve[CoordT], CoordT]: - return self.point_type.encode_to_curve(alpha_string, salt, self.curve) - - def msm( - self, - points: list[CurvePoint[Curve[CoordT], CoordT]], - scalars: list[int], - ) -> CurvePoint[Curve[CoordT], CoordT]: - return self.point_type.msm(points, scalars, self.curve) diff --git a/dot_ring/curve/glv.py b/dot_ring/curve/glv.py index 132fefb..ef365bd 100644 --- a/dot_ring/curve/glv.py +++ b/dot_ring/curve/glv.py @@ -172,7 +172,7 @@ def compute_endomorphism(self, point: AffinePointT) -> AffinePointT: x, y, p = point.x, point.y, point.curve.params.field_modulus if x is None or y is None: - return point.__class__.identity(point.curve) + return point.__class__.identity() y2 = pow(cast(int, y), 2, p) xy = (cast(int, x) * cast(int, y)) % p f_y = (self.constant_c * (1 - y2)) % p @@ -186,7 +186,7 @@ def compute_endomorphism(self, point: AffinePointT) -> AffinePointT: x_a = (x_p * pow(z_p, -1, p)) % p y_a = (y_p * pow(z_p, -1, p)) % p - return point.__class__(x_a, y_a, point.curve) + return point.__class__(x_a, y_a) def windowed_simultaneous_mult(self, k1: int, k2: int, P1: AffinePointT, P2: AffinePointT, w: int = 2) -> AffinePointT: """ @@ -245,7 +245,7 @@ def windowed_simultaneous_mult(self, k1: int, k2: int, P1: AffinePointT, P2: Aff # Convert back to affine ax, ay = projective_to_affine(rx, ry, rz, p) point_cls = cast(type[AffinePointT], P1.__class__) - return point_cls(ax, ay, P1.curve) + return point_cls(ax, ay) def multi_scalar_mult_4( self, @@ -288,14 +288,14 @@ def multi_scalar_mult_4( if P1.is_identity() or P2.is_identity() or P3.is_identity() or P4.is_identity(): point_cls = cast(type[AffinePointT], P1.__class__) - result = point_cls.identity(P1.curve) + result = point_cls.identity() for scalar, point in ((k1, P1), (k2, P2), (k3, P3), (k4, P4)): if scalar != 0 and not point.is_identity(): result = result + point * scalar # type: ignore[operator] return result if w != 2: point_cls = cast(type[AffinePointT], P1.__class__) - result = point_cls.identity(P1.curve) + result = point_cls.identity() for scalar, point in ((k1, P1), (k2, P2), (k3, P3), (k4, P4)): if scalar: result = result + point * scalar # type: ignore[operator] @@ -347,7 +347,7 @@ def multi_scalar_mult_4( ax, ay = projective_to_affine(rx, ry, rz, p) point_cls = cast(type[AffinePointT], P1.__class__) - return point_cls(ax, ay, P1.curve) + return point_cls(ax, ay) def multi_scalar_mult_pippenger( self, @@ -362,7 +362,7 @@ def multi_scalar_mult_pippenger( d_coeff = points[0].curve.params.d ax, ay = _native_signed_pippenger_msm(points, scalars, a_coeff, d_coeff, p, window_bits, True) point_cls = cast(type[AffinePointT], points[0].__class__) - return point_cls(ax, ay, points[0].curve) + return point_cls(ax, ay) def multi_scalar_mult_6( self, @@ -387,7 +387,7 @@ def multi_scalar_mult_6( if any(point.is_identity() for point in work_points): point_cls = cast(type[AffinePointT], work_points[0].__class__) - result = point_cls.identity(work_points[0].curve) + result = point_cls.identity() for scalar, point in zip(work_scalars, work_points, strict=True): if scalar != 0 and not point.is_identity(): result = result + point * scalar # type: ignore[operator] @@ -409,7 +409,7 @@ def multi_scalar_mult_6( or P6.y is None ): point_cls = cast(type[AffinePointT], P1.__class__) - result = point_cls.identity(P1.curve) + result = point_cls.identity() for scalar, point in zip(work_scalars, work_points, strict=True): result = result + point * scalar # type: ignore[operator] return result @@ -427,7 +427,7 @@ def multi_scalar_mult_6( if w != 2: point_cls = cast(type[AffinePointT], P1.__class__) - result = point_cls.identity(P1.curve) + result = point_cls.identity() for scalar, point in zip(work_scalars, work_points, strict=True): result = result + point * scalar # type: ignore[operator] return result @@ -469,4 +469,4 @@ def multi_scalar_mult_6( ) ax, ay = projective_to_affine(rx, ry, rz, p) point_cls = cast(type[AffinePointT], P1.__class__) - return point_cls(ax, ay, P1.curve) + return point_cls(ax, ay) diff --git a/dot_ring/curve/montgomery/mg_affine_point.py b/dot_ring/curve/montgomery/mg_affine_point.py index 7c74e90..24ddc4d 100644 --- a/dot_ring/curve/montgomery/mg_affine_point.py +++ b/dot_ring/curve/montgomery/mg_affine_point.py @@ -65,7 +65,7 @@ def __add__(self, other: MGAffinePoint[C]) -> MGAffinePoint[C]: # ensure coords are ints mod p if self.x is None or self.y is None or other.x is None or other.y is None: # Should be handled by is_identity checks above, but for mypy: - return self.__class__(None, None, self.curve) + return self.__class__(None, None) x1, y1 = cast(int, self.x) % p, cast(int, self.y) % p x2, y2 = cast(int, other.x) % p, cast(int, other.y) % p @@ -74,21 +74,21 @@ def __add__(self, other: MGAffinePoint[C]) -> MGAffinePoint[C]: if x1 == x2 and y1 == y2: # if y == 0 then slope denominator = 0 => result is identity if y1 % p == 0: - return self.__class__(None, None, self.curve) + return self.__class__(None, None) numerator = (3 * cast(int, x1) * cast(int, x1) + 2 * cast(int, A) * cast(int, x1) + 1) % p denominator = (2 * cast(int, B) * cast(int, y1)) % p # Check if denominator is zero before computing inverse if denominator == 0: - return self.__class__(None, None, self.curve) + return self.__class__(None, None) lam = (numerator * pow(denominator, -1, p)) % p x3 = (cast(int, B) * (lam * lam % p) - cast(int, A) - 2 * cast(int, x1)) % p y3 = (lam * (cast(int, x1) - x3) - cast(int, y1)) % p - return self.__class__(x3, y3, self.curve) + return self.__class__(x3, y3) # Addition for distinct points if x1 == x2: # vertical line -> identity (x1 == x2 but y1 != y2) - return self.__class__(None, None, self.curve) + return self.__class__(None, None) numerator = (cast(int, y2) - cast(int, y1)) % p denominator = (cast(int, x2) - cast(int, x1)) % p @@ -100,16 +100,15 @@ def __add__(self, other: MGAffinePoint[C]) -> MGAffinePoint[C]: x3 = (cast(int, B) * lam * lam - cast(int, A) - cast(int, x1) - cast(int, x2)) % p # Corrected formula for y3 y3 = (lam * (cast(int, x1) - x3) - cast(int, y1)) % p - return self.__class__(x3, y3, self.curve) + return self.__class__(x3, y3) def __neg__(self) -> MGAffinePoint[C]: """Negate a point (x, y) -> (x, -y).""" if self.is_identity() or self.x is None or self.y is None: - return self.__class__(None, None, self.curve) + return self.__class__(None, None) return self.__class__( cast(int, self.x) % self.curve.params.field_modulus, (-cast(int, self.y)) % self.curve.params.field_modulus, - self.curve, ) def __sub__(self, other: MGAffinePoint[C]) -> MGAffinePoint[C]: @@ -191,14 +190,14 @@ def __mul__(self, scalar: int) -> MGAffinePoint[C]: by using the same underlying operations. """ if scalar == 0: - return self.__class__(None, None, self.curve) + return self.__class__(None, None) # Handle negative scalar if scalar < 0: return (-self) * (-scalar) if self.is_identity(): - return self.__class__(None, None, self.curve) + return self.__class__(None, None) # Use double-and-add for consistency with point addition formulas return self._scalar_mult_double_add(scalar) @@ -211,16 +210,16 @@ def _scalar_mult_double_add(self, scalar: int) -> MGAffinePoint[C]: by using the same underlying operations. """ if scalar == 0: - return self.__class__(None, None, self.curve) + return self.__class__(None, None) # Handle negative scalar if scalar < 0: return (-self)._scalar_mult_double_add(-scalar) if self.is_identity(): - return self.__class__(None, None, self.curve) + return self.__class__(None, None) - result = self.__class__(None, None, self.curve) # Start with identity + result = self.__class__(None, None) # Start with identity current = self # Convert scalar to binary and process each bit @@ -243,63 +242,51 @@ def clear_cofactor(self) -> MGAffinePoint[C]: return self * self.curve.params.cofactor @classmethod - def identity(cls, curve: C) -> MGAffinePoint: + def identity(cls) -> MGAffinePoint: """Get the identity element (point at infinity).""" - return cls(None, None, curve) + return cls(None, None) @classmethod def encode_to_curve( cls, - alpha_string: bytes | str, - salt: bytes | str = b"", - curve: C | None = None, + alpha_string: bytes, + salt: bytes = b"", ) -> MGAffinePoint[C]: - if curve is None: - raise ValueError("curve is required") - if not isinstance(alpha_string, bytes): - alpha_string = bytes.fromhex(alpha_string) - - if not isinstance(salt, bytes): - salt = bytes.fromhex(salt) - - # Check if it's an ELL2 variant (ELL2 or ELL2_NU) - if curve.e2c_variant in (E2C_Variant.ELL2, E2C_Variant.ELL2_NU): - if curve.e2c_variant.value.endswith("_NU_"): - return cls._encode_elligator2_nu(alpha_string, curve, salt) - - return cls._encode_elligator2_ro(alpha_string, curve, salt) - - raise ValueError(f"Unexpected E2C Variant: {curve.e2c_variant}") + match cls.curve.e2c_variant: + case E2C_Variant.ELL2_NU: + return cls._encode_elligator2_nu(alpha_string, salt) + case E2C_Variant.ELL2: + return cls._encode_elligator2_ro(alpha_string, salt) + case _: + return super().encode_to_curve(alpha_string, salt) @classmethod def _encode_elligator2_nu( cls, alpha_string: bytes, - curve: C, salt: bytes = b"", ) -> MGAffinePoint[C]: """Encode with the nonuniform Elligator2 hash-to-curve variant.""" string_to_hash = salt + alpha_string - (u0,) = curve.hash_to_field(string_to_hash, 1) - return cls.map_to_curve(u0, curve).clear_cofactor() + (u0,) = cls.curve.hash_to_field(string_to_hash, 1) + return cls.map_to_curve(u0).clear_cofactor() @classmethod def _encode_elligator2_ro( cls, alpha_string: bytes, - curve: C, salt: bytes = b"", ) -> MGAffinePoint[C]: """Encode with the random-oracle Elligator2 hash-to-curve variant.""" string_to_hash = salt + alpha_string - u0, u1 = curve.hash_to_field(string_to_hash, 2) - q0 = cls.map_to_curve(u0, curve) - q1 = cls.map_to_curve(u1, curve) + u0, u1 = cls.curve.hash_to_field(string_to_hash, 2) + q0 = cls.map_to_curve(u0) + q1 = cls.map_to_curve(u1) R = q0 + q1 return R.clear_cofactor() @classmethod - def map_to_curve(cls, u: int, curve: C) -> MGAffinePoint[C]: + def map_to_curve(cls, u: int) -> MGAffinePoint[C]: """ Map a field element to a curve point using Elligator 2. @@ -320,6 +307,7 @@ def map_to_curve(cls, u: int, curve: C) -> MGAffinePoint[C]: # 9. t = y * K # 10.return (s, t) + curve = cls.curve p = curve.params.field_modulus J = curve.params.a K = curve.params.b @@ -355,7 +343,7 @@ def map_to_curve(cls, u: int, curve: C) -> MGAffinePoint[C]: s = (x * K) % p t = (y * K) % p - return cls(s, t, curve) + return cls(s, t) def point_to_string(self) -> bytes: if self.is_identity(): @@ -374,22 +362,22 @@ def point_to_string(self) -> bytes: raise NotImplementedError("Compressed encoding not implemented") @classmethod - def string_to_point(cls, data: str | bytes, curve: C) -> MGAffinePoint: + def string_to_point(cls, data: str | bytes) -> MGAffinePoint: if isinstance(data, str): data = bytes.fromhex(data) - p = curve.params.field_modulus + p = cls.curve.params.field_modulus byte_length = (p.bit_length() + 7) // 8 - if curve.params.encoding.uncompressed: + if cls.curve.params.encoding.uncompressed: # Split into u and v coordinates u_bytes = data[:byte_length] v_bytes = data[byte_length:] - u = int.from_bytes(u_bytes, curve.encoding_endian()) - v = int.from_bytes(v_bytes, curve.encoding_endian()) + u = int.from_bytes(u_bytes, cls.curve.encoding_endian()) + v = int.from_bytes(v_bytes, cls.curve.encoding_endian()) # Create the point - point = cls(u, v, curve) + point = cls(u, v) else: ... @@ -401,5 +389,5 @@ def string_to_point(cls, data: str | bytes, curve: C) -> MGAffinePoint: return point @classmethod - def _x_recover(cls, y: int, curve: C) -> int: + def _x_recover(cls, y: int) -> int: raise NotImplementedError("Must be implemented by subclass") diff --git a/dot_ring/curve/montgomery/mg_curve.py b/dot_ring/curve/montgomery/mg_curve.py index 8f0879c..0c5fe62 100644 --- a/dot_ring/curve/montgomery/mg_curve.py +++ b/dot_ring/curve/montgomery/mg_curve.py @@ -1,17 +1,11 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, TypeVar +from typing import Any from ..curve import Curve from ..specs.parameters import MontgomeryCurveParams -if TYPE_CHECKING: - from .mg_affine_point import MGAffinePoint - -C = TypeVar("C", bound="MGCurve") -P = TypeVar("P", bound="MGAffinePoint") - @dataclass(frozen=True, kw_only=True) class MGCurve(Curve[int]): @@ -52,53 +46,6 @@ def is_on_curve(self, point: tuple[int, int]) -> bool: right = (u * u * u + self.params.a * u * u + u) % p return left == right - def point_at_infinity(self) -> MGAffinePoint: - """Return the point at infinity for this curve.""" - # Import here to avoid circular dependency - from .mg_affine_point import MGAffinePoint - - return MGAffinePoint(None, None, self) - - def random_point(self, rng: Any = None) -> MGAffinePoint: - """ - Generate a random point on the curve by trying random x-coordinates - until we find one that gives a valid y-coordinate. - """ - import secrets - - if rng is None: - rng = secrets.SystemRandom() - - from .mg_affine_point import MGAffinePoint - - p = self.params.field_modulus - max_attempts = 100 - - for _ in range(max_attempts): - # Try random x-coordinate - x = rng.randrange(0, p) - - # Compute y² = (x³ + Ax² + x) / B - x_cubed = (x * x * x) % p - x_squared = (x * x) % p - numerator = (x_cubed + (self.params.a * x_squared) % p + x) % p - - try: - inv_B = pow(self.params.b, -1, p) - y_squared = (numerator * inv_B) % p - - # Check if y_squared is a quadratic residue - if pow(y_squared, (p - 1) // 2, p) == 1: - # Create temporary point to use _sqrt_mod_p method - temp_point = MGAffinePoint(0, 0, self) - y = temp_point._sqrt_mod_p(y_squared) - if y is not None: - return MGAffinePoint(x, y, self) - except ValueError: - continue - - raise RuntimeError(f"Failed to find random point after {max_attempts} attempts") - def validate_point(self, point: Any) -> bool: """ Validate that a point is properly constructed and on the curve. diff --git a/dot_ring/curve/point.py b/dot_ring/curve/point.py index 451e07e..69af492 100644 --- a/dot_ring/curve/point.py +++ b/dot_ring/curve/point.py @@ -3,6 +3,7 @@ from abc import abstractmethod from typing import TYPE_CHECKING, Generic, Self, TypeVar, cast +from dot_ring.curve.e2c import E2C_Variant from dot_ring.curve.fp2 import Fp2 if TYPE_CHECKING: @@ -11,6 +12,8 @@ # TypeVar for curve types Coord = int | Fp2 CoordT = TypeVar("CoordT", int, Fp2) + + C = TypeVar("C", bound="Curve[Coord]") @@ -35,11 +38,10 @@ def __init__( self, x: CoordT | None, y: CoordT | None, - curve: C, ) -> None: self.x = x self.y = y - self.curve = curve + self.curve = self.__class__.curve super().__init__() self.__post_init__() @@ -82,7 +84,7 @@ def _validate_coordinates(self) -> bool: return 0 <= cast(int, self.x) < field_modulus and 0 <= cast(int, self.y) < field_modulus @classmethod - def msm(cls, points: list[Self], scalars: list[int], curve: C) -> Self: + def msm(cls, points: list[Self], scalars: list[int]) -> Self: """ Compute sum(P_i * s_i) for pairs (P_i, s_i). Default implementation uses naive summation. @@ -92,24 +94,24 @@ def msm(cls, points: list[Self], scalars: list[int], curve: C) -> Self: raise ValueError("Points and scalars must have same length") if not points: - return cls.identity(curve) + return cls.identity() - result = cls.identity(curve) + result = cls.identity() for point, scalar in zip(points, scalars, strict=False): result = result + (point * scalar) return result @classmethod - def generator_point(cls, curve: C) -> Self: + def generator_point(cls) -> Self: """ Get the generator point of the curve. Returns: BandersnatchPoint: Generator point """ - generator_x, generator_y = curve.params.generator - return cls(generator_x, generator_y, curve) + generator_x, generator_y = cls.curve.params.generator + return cls(generator_x, generator_y) @abstractmethod def is_on_curve(self) -> bool: @@ -136,7 +138,7 @@ def is_identity(self) -> bool: @classmethod @abstractmethod - def identity(cls, curve: C) -> Self: + def identity(cls) -> Self: """ Get the identity element. @@ -173,7 +175,7 @@ def point_to_string(self) -> bytes: return encoded @classmethod - def string_to_point(cls, octet_string: str | bytes, curve: C) -> Self: + def string_to_point(cls, octet_string: str | bytes) -> Self: """ Convert compressed octet string back to point. Args: @@ -186,8 +188,9 @@ def string_to_point(cls, octet_string: str | bytes, curve: C) -> Self: ValueError: If encoding is invalid. """ + curve = cls.curve if curve.params.encoding.uncompressed: - return cls.uncompressed_s2p(octet_string, curve) + return cls.uncompressed_s2p(octet_string) if isinstance(octet_string, str): octet_string = bytes.fromhex(octet_string) @@ -201,7 +204,7 @@ def string_to_point(cls, octet_string: str | bytes, curve: C) -> Self: if y >= curve.params.field_modulus: raise ValueError("Invalid point encoding") - x_candidates = cls._x_recover(y, curve) + x_candidates = cls._x_recover(y) if x_candidates is None: raise ValueError("Invalid point encoding") @@ -211,7 +214,7 @@ def string_to_point(cls, octet_string: str | bytes, curve: C) -> Self: x, neg_x = x_candidates x, y = neg_x if x_sign_bit else x, y - return cls(x, y, curve) + return cls(x, y) def uncompressed_p2s(self) -> bytes: p = self.curve.params.field_modulus @@ -222,7 +225,8 @@ def uncompressed_p2s(self) -> bytes: return x_bytes + y_bytes @classmethod - def uncompressed_s2p(cls, octet_string: str | bytes, curve: C) -> Self: + def uncompressed_s2p(cls, octet_string: str | bytes) -> Self: + curve = cls.curve if isinstance(octet_string, str): octet_string = bytes.fromhex(octet_string) p = curve.params.field_modulus @@ -233,14 +237,14 @@ def uncompressed_s2p(cls, octet_string: str | bytes, curve: C) -> Self: x = int.from_bytes(x_bytes, curve.encoding_endian()) y = int.from_bytes(y_bytes, curve.encoding_endian()) # Create the point - point = cls(x % curve.params.field_modulus, y % curve.params.field_modulus, curve) + point = cls(x % curve.params.field_modulus, y % curve.params.field_modulus) # Verify the point is on the curve if not point.is_on_curve(): raise ValueError("Point is not on the curve") return point @classmethod - def _x_recover(cls, y: int, curve: C) -> int | tuple[int, int] | None: + def _x_recover(cls, y: int) -> int | tuple[int, int] | None: """ Recover x-coordinate from y. @@ -258,11 +262,49 @@ def _x_recover(cls, y: int, curve: C) -> int | tuple[int, int] | None: @classmethod def encode_to_curve( cls, - alpha_string: bytes | str, - salt: bytes | str = b"", - curve: C | None = None, + alpha_string: bytes, + salt: bytes = b"", ) -> Self: - raise NotImplementedError("Must be implemented by subclass") + curve = cls.curve + if curve.e2c_variant != E2C_Variant.TAI: + raise ValueError(f"Unexpected E2C Variant: {curve.e2c_variant}") + + from dot_ring.vrf.codec import enc_64 + from dot_ring.vrf.domain import DomSep + from dot_ring.vrf.primitives import VrfTranscript + + data = salt + alpha_string + field_len = (curve.params.field_modulus.bit_length() + 7) // 8 + + prefix = VrfTranscript(curve.params.suite_id, curve.params.hash_fn) + prefix.absorb(bytes([DomSep.HASH_TO_CURVE])) + prefix.absorb(enc_64(len(data))) + prefix.absorb(data) + + for counter in range(256): + t = prefix.copy() + t.absorb(bytes([counter])) + candidate = bytearray(t.squeeze(field_len)) + if hasattr(curve.params, "a") and hasattr(curve.params, "b"): + shave = field_len * 8 - curve.params.field_modulus.bit_length() + if shave: + candidate[-1] &= (1 << (8 - shave)) - 1 + candidate = bytearray(candidate + b"\x80") + else: + sign = candidate[-1] & 0x80 + shave = field_len * 8 - curve.params.field_modulus.bit_length() + if shave: + candidate[-1] &= (1 << (8 - shave)) - 1 + candidate[-1] |= sign + try: + point = cls.string_to_point(bytes(candidate)) + except ValueError: + continue + if point.curve.params.cofactor > 1: + point = point * point.curve.params.cofactor + if not point.is_identity(): + return point + raise ValueError("hash_to_curve_tai failed") def __hash__(self) -> int: if self.x is None or self.y is None: diff --git a/dot_ring/curve/short_weierstrass/sw_affine_point.py b/dot_ring/curve/short_weierstrass/sw_affine_point.py index f9f7403..84553f7 100644 --- a/dot_ring/curve/short_weierstrass/sw_affine_point.py +++ b/dot_ring/curve/short_weierstrass/sw_affine_point.py @@ -37,7 +37,7 @@ def __add__(self, other: SWAffinePoint) -> Self: return self if self.x is None or self.y is None or other.x is None or other.y is None: - return self.__class__.identity(self.curve) + return self.__class__.identity() x1 = self.x y1 = self.y @@ -47,7 +47,7 @@ def __add__(self, other: SWAffinePoint) -> Self: # Check if points are inverses if x1 == x2 and (y1 + y2) % p == 0: - return self.__class__.identity(self.curve) + return self.__class__.identity() # Point doubling if x1 == x2 and y1 == y2: @@ -64,7 +64,7 @@ def __add__(self, other: SWAffinePoint) -> Self: y3 = (slope * (x1 - x3) - y1) % p # The curve is already set in the instance - return self.__class__(x3, y3, self.curve) + return self.__class__(x3, y3) def _double(self) -> Self: """ @@ -86,7 +86,7 @@ def _double(self) -> Self: # Check if y-coordinate is zero (point has order 2) if y1 == 0: - return self.__class__.identity(self.curve) + return self.__class__.identity() # Calculate slope: λ = (3x₁² + a) / (2y₁) numerator = (3 * x1 * x1 + A) % p @@ -98,7 +98,7 @@ def _double(self) -> Self: y3 = (slope * (x1 - x3) - y1) % p # The curve is already set in the instance - return self.__class__(x3, y3, self.curve) + return self.__class__(x3, y3) def __mul__(self, scalar: int) -> Self: """ @@ -111,13 +111,13 @@ def __mul__(self, scalar: int) -> Self: Self: Result of scalar multiplication """ if scalar == 0: - return self.__class__.identity(self.curve) + return self.__class__.identity() if scalar < 0: return (-self) * (-scalar) # Use binary method (double-and-add) - result = self.__class__.identity(self.curve) + result = self.__class__.identity() addend = self while scalar > 0: @@ -138,11 +138,11 @@ def __neg__(self) -> Self: if self.is_identity(): return self if self.x is None or self.y is None: - return self.__class__.identity(self.curve) + return self.__class__.identity() # Create a new instance with negated y-coordinate # The curve is already set in the instance - return self.__class__(self.x, (-self.y) % self.curve.params.field_modulus, self.curve) + return self.__class__(self.x, (-self.y) % self.curve.params.field_modulus) def __sub__(self, other: SWAffinePoint) -> Self: """ @@ -189,7 +189,8 @@ def point_to_string(self, compressed: bool = True) -> bytes: return b"\x04" + x_bytes + y_bytes @classmethod - def string_to_point(cls, octet_string: str | bytes, curve: SWCurve[int]) -> Self: + def string_to_point(cls, octet_string: str | bytes) -> Self: + curve = cls.curve if isinstance(octet_string, str): octet_string = bytes.fromhex(octet_string) @@ -202,7 +203,7 @@ def string_to_point(cls, octet_string: str | bytes, curve: SWCurve[int]) -> Self if prefix == 0x00: if len(octet_string) != 1: raise ValueError("Point at infinity must be single byte 0x00") - return cls.identity(curve) + return cls.identity() p = curve.params.field_modulus A = curve.params.a @@ -237,7 +238,7 @@ def string_to_point(cls, octet_string: str | bytes, curve: SWCurve[int]) -> Self if (y % 2 == 0 and prefix == 0x03) or (y % 2 == 1 and prefix == 0x02): y = p - y # Use the other square root - point = cls(x, y, curve) + point = cls(x, y) # Verify point is on curve if not point.is_on_curve(): @@ -264,7 +265,7 @@ def string_to_point(cls, octet_string: str | bytes, curve: SWCurve[int]) -> Self if y >= p: raise ValueError(f"y-coordinate {y} is not in field Fp (p={p})") - point = cls(x, y, curve) + point = cls(x, y) # Verify point is on curve if not point.is_on_curve(): @@ -293,7 +294,7 @@ def string_to_point(cls, octet_string: str | bytes, curve: SWCurve[int]) -> Self if (y % 2 == 0 and prefix == 0x07) or (y % 2 == 1 and prefix == 0x06): raise ValueError("Hybrid format: y parity doesn't match prefix") - point = cls(x, y, curve) + point = cls(x, y) if not point.is_on_curve(): raise ValueError(f"Point ({x}, {y}) is not on curve") @@ -351,7 +352,7 @@ def is_identity(self) -> bool: return self.x is None and self.y is None @classmethod - def identity(cls, curve: SWCurve[int]) -> Self: + def identity(cls) -> Self: """ Get the identity element (point at infinity). @@ -359,7 +360,7 @@ def identity(cls, curve: SWCurve[int]) -> Self: Self: Identity element """ # Return a point at infinity (None, None) - return cls(None, None, curve) + return cls(None, None) @staticmethod def tonelli_shanks(n: int, p: int) -> int | None: @@ -407,7 +408,8 @@ def tonelli_shanks(n: int, p: int) -> int | None: return r @classmethod - def _x_recover(cls, y: int, curve: SWCurve[int]) -> tuple[int, int]: + def _x_recover(cls, y: int) -> tuple[int, int]: + curve = cls.curve p = curve.params.field_modulus B = curve.params.b @@ -423,8 +425,9 @@ def _x_recover(cls, y: int, curve: SWCurve[int]) -> tuple[int, int]: return x, (-x) % p @classmethod - def map_to_curve_simple_swu(cls, u: int, curve: SWCurve[int]) -> Self: + def map_to_curve_simple_swu(cls, u: int) -> Self: """Implements simplified SWU mapping""" + curve = cls.curve # 1. tv1 = inv0(Z^2 * u^4 + Z * u^2) # 2. x1 = (-B / A) * (1 + tv1) # 3. If tv1 == 0, set x1 = B / (Z * A) @@ -486,49 +489,35 @@ def map_to_curve_simple_swu(cls, u: int, curve: SWCurve[int]) -> Self: # Check if point lies on E' if (y * y - (x**3 + map_curve.a * x + map_curve.b)) % p != 0: raise ValueError("Point is not on the hash-to-curve map curve") - return cls.apply_isogeny(x, y, curve) + return cls.apply_isogeny(x, y) - return cls(x=x, y=y, curve=curve) + return cls(x=x, y=y) @classmethod def encode_to_curve( cls, - alpha_string: bytes | str, - salt: bytes | str = b"", - curve: SWCurve[int] | None = None, + alpha_string: bytes, + salt: bytes = b"", ) -> Self: - if curve is None: - raise ValueError("curve is required") - if not isinstance(alpha_string, bytes): - alpha_string = bytes.fromhex(alpha_string) - - if not isinstance(salt, bytes): - salt = bytes.fromhex(salt) - - if curve.e2c_variant == E2C_Variant.TAI: - from dot_ring.vrf.transcript import hash_to_curve_tai - - return cast(Self, hash_to_curve_tai(cls, salt + alpha_string, curve)) - - if curve.e2c_variant in (E2C_Variant.SSWU, E2C_Variant.SSWU_NU): - if curve.e2c_variant.value.endswith("_NU_"): - return cls._encode_sswu_nu(alpha_string, curve, salt) - return cls._encode_sswu_ro(alpha_string, curve, salt) - - raise ValueError(f"Unexpected E2C Variant: {curve.e2c_variant}") + match cls.curve.e2c_variant: + case E2C_Variant.SSWU_NU: + return cls._encode_sswu_nu(alpha_string, salt) + case E2C_Variant.SSWU: + return cls._encode_sswu_ro(alpha_string, salt) + case _: + return super().encode_to_curve(alpha_string, salt) @classmethod def _encode_sswu_ro( cls, alpha_string: bytes, - curve: SWCurve[int], salt: bytes = b"", ) -> Self: """Encode with the random-oracle simplified-SWU hash-to-curve variant.""" string_to_hash = salt + alpha_string - u0, u1 = curve.hash_to_field(string_to_hash, 2) - q0 = cls.map_to_curve_simple_swu(u0, curve) - q1 = cls.map_to_curve_simple_swu(u1, curve) + u0, u1 = cls.curve.hash_to_field(string_to_hash, 2) + q0 = cls.map_to_curve_simple_swu(u0) + q1 = cls.map_to_curve_simple_swu(u1) R = q0 + q1 return cast(Self, R.clear_cofactor()) @@ -536,21 +525,20 @@ def _encode_sswu_ro( def _encode_sswu_nu( cls, alpha_string: bytes, - curve: SWCurve[int], salt: bytes = b"", ) -> Self: """Encode with the nonuniform simplified-SWU hash-to-curve variant.""" string_to_hash = salt + alpha_string - (u0,) = curve.hash_to_field(string_to_hash, 1) - return cast(Self, cls.map_to_curve_simple_swu(u0, curve).clear_cofactor()) + (u0,) = cls.curve.hash_to_field(string_to_hash, 1) + return cast(Self, cls.map_to_curve_simple_swu(u0).clear_cofactor()) @classmethod - def apply_isogeny(cls, x_p: int, y_p: int, curve: SWCurve[int]) -> Self: + def apply_isogeny(cls, x_p: int, y_p: int) -> Self: """ Apply the rational isogeny map to a point (x', y') on the isogenous curve E'. """ - p = curve.params.field_modulus - isogeny = curve.params.hash_to_curve.isogeny + p = cls.curve.params.field_modulus + isogeny = cls.curve.params.hash_to_curve.isogeny if isogeny is None: raise ValueError("Missing isogeny") @@ -571,4 +559,4 @@ def evaluate(coefficients: tuple[int, ...], x: int) -> int: x_mapped = (x_num * x_den_inv) % p y_mapped = (y_p * y_num * y_den_inv) % p - return cls(x_mapped, y_mapped, curve) + return cls(x_mapped, y_mapped) diff --git a/dot_ring/curve/specs/baby_jubjub.py b/dot_ring/curve/specs/baby_jubjub.py index d8ca964..b2bcb39 100644 --- a/dot_ring/curve/specs/baby_jubjub.py +++ b/dot_ring/curve/specs/baby_jubjub.py @@ -52,8 +52,15 @@ ) +BabyJubJub_Curve = TECurve(params=BABY_JUBJUB_PARAMS, e2c_variant=E2C_Variant.TAI) + + +class BabyJubJubPoint(TEAffinePoint): + curve = BabyJubJub_Curve + + BabyJubJub = CurveVariant( name="BabyJubJub", - curve=TECurve(params=BABY_JUBJUB_PARAMS, e2c_variant=E2C_Variant.TAI), - point_type=TEAffinePoint, + curve=BabyJubJub_Curve, + point_type=BabyJubJubPoint, ) diff --git a/dot_ring/curve/specs/bandersnatch.py b/dot_ring/curve/specs/bandersnatch.py index b18d412..a682452 100644 --- a/dot_ring/curve/specs/bandersnatch.py +++ b/dot_ring/curve/specs/bandersnatch.py @@ -172,6 +172,8 @@ class BandersnatchPoint(TEAffinePoint[TECurve]): including GLV scalar multiplication. """ + curve = Bandersnatch_TE_Curve + def __mul__(self, scalar: int) -> Self: """ GLV scalar multiplication using endomorphism. @@ -189,17 +191,17 @@ def __mul__(self, scalar: int) -> Self: return cast(Self, BandersnatchGLV.windowed_simultaneous_mult(k1, k2, self, phi, w=2)) @classmethod - def msm(cls, points: list[Self], scalars: list[int], curve: TECurve) -> Self: + def msm(cls, points: list[Self], scalars: list[int]) -> Self: """ Optimized multi-scalar multiplication using GLV. """ if len(points) != len(scalars): raise ValueError("Points and scalars must have same length") if not points: - return cls.identity(curve) + return cls.identity() # Normalize scalars to [0, ORDER) for GLV - n = curve.params.subgroup_order + n = cls.curve.params.subgroup_order raw_scalars = scalars scalars = [s % n for s in scalars] @@ -221,7 +223,7 @@ def msm(cls, points: list[Self], scalars: list[int], curve: TECurve) -> Self: if len(points) == 4: if any(point.is_identity() for point in points): - return super().msm(points, scalars, curve) + return super().msm(points, scalars) return cast( Self, @@ -239,7 +241,7 @@ def msm(cls, points: list[Self], scalars: list[int], curve: TECurve) -> Self: if len(points) == 3: if any(point.is_identity() for point in points): - return super().msm(points, scalars, curve) + return super().msm(points, scalars) decomposed = [BandersnatchGLV.decompose_scalar(scalar, n) for scalar in scalars] phi_points = [BandersnatchGLV.compute_endomorphism(point) for point in points] @@ -276,12 +278,12 @@ def msm(cls, points: list[Self], scalars: list[int], curve: TECurve) -> Self: work_points.append(point) work_scalars.append(scalar) if not work_points: - return cls.identity(curve) + return cls.identity() if len(work_points) >= 5: window_bits = _pippenger_window_bits(len(work_points)) return cast(Self, BandersnatchGLV.multi_scalar_mult_pippenger(work_scalars, work_points, window_bits=window_bits)) - return super().msm(points, scalars, curve) + return super().msm(points, scalars) Bandersnatch = CurveVariant( @@ -292,8 +294,13 @@ def msm(cls, points: list[Self], scalars: list[int], curve: TECurve) -> Self: Bandersnatch_SHAKE128_TE_Curve = BandersnatchCurve(params=BANDERSNATCH_SHAKE128_PARAMS, e2c_variant=E2C_Variant.ELL2) + +class BandersnatchSHAKE128Point(BandersnatchPoint): + curve = Bandersnatch_SHAKE128_TE_Curve + + Bandersnatch_SHAKE128 = CurveVariant( name="Bandersnatch_SHAKE128", curve=Bandersnatch_SHAKE128_TE_Curve, - point_type=BandersnatchPoint, + point_type=BandersnatchSHAKE128Point, ) diff --git a/dot_ring/curve/specs/bandersnatch_sw.py b/dot_ring/curve/specs/bandersnatch_sw.py index d0244fc..e902a87 100644 --- a/dot_ring/curve/specs/bandersnatch_sw.py +++ b/dot_ring/curve/specs/bandersnatch_sw.py @@ -53,9 +53,14 @@ ) +BANDERSNATCH_SW_Curve = SWCurve(params=BANDERSNATCH_SW_PARAMS, e2c_variant=E2C_Variant.TAI) + + class BandersnatchSWPoint(SWAffinePoint): """Point on Bandersnatch in short-Weierstrass form.""" + curve = BANDERSNATCH_SW_Curve + def point_to_string(self, compressed: bool = False) -> bytes: p = self.curve.params.field_modulus field_bit_len = p.bit_length() @@ -79,7 +84,8 @@ def point_to_string(self, compressed: bool = False) -> bytes: return bytes(result) @classmethod - def _y_recover(cls, x: int, curve: SWCurve) -> tuple[int, int] | None: + def _y_recover(cls, x: int) -> tuple[int, int] | None: + curve = cls.curve p = curve.params.field_modulus y_square = (pow(x, 3, p) + curve.params.a * x + curve.params.b) % p try: @@ -95,7 +101,8 @@ def _y_recover(cls, x: int, curve: SWCurve) -> tuple[int, int] | None: return neg_y, y @classmethod - def string_to_point(cls, data: str | bytes, curve: SWCurve) -> Self: + def string_to_point(cls, data: str | bytes) -> Self: + curve = cls.curve if isinstance(data, str): data = bytes.fromhex(data) @@ -104,7 +111,7 @@ def string_to_point(cls, data: str | bytes, curve: SWCurve) -> Self: x_bytes = data[:-1] x = int.from_bytes(x_bytes, curve.encoding_endian()) - y_candidates = cls._y_recover(x, curve) + y_candidates = cls._y_recover(x) if not y_candidates: raise ValueError("Invalid point: no y-coordinate found for x") y, y_neg = y_candidates @@ -121,13 +128,13 @@ def string_to_point(cls, data: str | bytes, curve: SWCurve) -> Self: raise ValueError("Invalid infinity point: not supported") try: - return cls(x, y_neg if is_negative else y, curve) + return cls(x, y_neg if is_negative else y) except ValueError: raise ValueError("Invalid point") from None Bandersnatch_SW = CurveVariant( name="Bandersnatch_SW", - curve=SWCurve(params=BANDERSNATCH_SW_PARAMS, e2c_variant=E2C_Variant.TAI), + curve=BANDERSNATCH_SW_Curve, point_type=BandersnatchSWPoint, ) diff --git a/dot_ring/curve/specs/bls12_381_G1.py b/dot_ring/curve/specs/bls12_381_G1.py index b9846d5..42143d8 100644 --- a/dot_ring/curve/specs/bls12_381_G1.py +++ b/dot_ring/curve/specs/bls12_381_G1.py @@ -119,14 +119,26 @@ def _descending_hex(*coefficients: str) -> tuple[int, ...]: ) +BLS12_381_G1_NU_Curve = SWCurve(params=BLS12_381_G1_PARAMS, e2c_variant=E2C_Variant.SSWU_NU) +BLS12_381_G1_RO_Curve = SWCurve(params=BLS12_381_G1_PARAMS, e2c_variant=E2C_Variant.SSWU) + + +class BLS12_381_G1_NUPoint(SWAffinePoint): + curve = BLS12_381_G1_NU_Curve + + +class BLS12_381_G1_ROPoint(SWAffinePoint): + curve = BLS12_381_G1_RO_Curve + + BLS12_381_G1_NU = CurveVariant( name="BLS12_381_G1_NU", - curve=SWCurve(params=BLS12_381_G1_PARAMS, e2c_variant=E2C_Variant.SSWU_NU), - point_type=SWAffinePoint, + curve=BLS12_381_G1_NU_Curve, + point_type=BLS12_381_G1_NUPoint, ) BLS12_381_G1_RO = CurveVariant( name="BLS12_381_G1_RO", - curve=SWCurve(params=BLS12_381_G1_PARAMS, e2c_variant=E2C_Variant.SSWU), - point_type=SWAffinePoint, + curve=BLS12_381_G1_RO_Curve, + point_type=BLS12_381_G1_ROPoint, ) diff --git a/dot_ring/curve/specs/bls12_381_G2.py b/dot_ring/curve/specs/bls12_381_G2.py index 2893942..98a89c8 100644 --- a/dot_ring/curve/specs/bls12_381_G2.py +++ b/dot_ring/curve/specs/bls12_381_G2.py @@ -143,19 +143,19 @@ class BLS12_381_G2Point(CurvePoint[SWCurve[Fp2], Fp2]): Implements point operations specific to the BLS12-381 G2 curve. """ - def __init__(self, x: Fp2 | tuple[int, int] | None, y: Fp2 | tuple[int, int] | None, curve: SWCurve[Fp2]) -> None: - super().__init__(self._coord(x, curve), self._coord(y, curve), curve) + def __init__(self, x: Fp2 | tuple[int, int] | None, y: Fp2 | tuple[int, int] | None) -> None: + super().__init__(self._coord(x), self._coord(y)) - @staticmethod - def _coord(value: Fp2 | tuple[int, int] | None, curve: SWCurve[Fp2]) -> Fp2 | None: + @classmethod + def _coord(cls, value: Fp2 | tuple[int, int] | None) -> Fp2 | None: if value is None: return None if isinstance(value, Fp2): - if value.p != curve.params.field_modulus: + if value.p != cls.curve.params.field_modulus: raise ValueError("Fp2 coordinate uses the wrong field") return value if isinstance(value, tuple) and len(value) == 2: - return Fp2(value[0], value[1], curve.params.field_modulus) + return Fp2(value[0], value[1], cls.curve.params.field_modulus) raise TypeError("BLS12-381 G2 coordinates must be Fp2 values") def _py_ecc_point(self) -> tuple[FQ2, FQ2]: @@ -164,10 +164,10 @@ def _py_ecc_point(self) -> tuple[FQ2, FQ2]: return self.x.to_fq2(), self.y.to_fq2() @classmethod - def _from_py_ecc_point(cls, point: tuple[FQ2, FQ2] | None, curve: SWCurve[Fp2]) -> BLS12_381_G2Point: + def _from_py_ecc_point(cls, point: tuple[FQ2, FQ2] | None) -> BLS12_381_G2Point: if point is None: - return cls.identity(curve) - return cls(Fp2.from_fq2(point[0], curve.params.field_modulus), Fp2.from_fq2(point[1], curve.params.field_modulus), curve) + return cls.identity() + return cls(Fp2.from_fq2(point[0], cls.curve.params.field_modulus), Fp2.from_fq2(point[1], cls.curve.params.field_modulus)) def _validate_coordinates(self) -> bool: if self.x is None and self.y is None: @@ -187,8 +187,8 @@ def is_identity(self) -> bool: return self.x is None and self.y is None @classmethod - def identity(cls, curve: SWCurve[Fp2]) -> BLS12_381_G2Point: - return cls(None, None, curve) + def identity(cls) -> BLS12_381_G2Point: + return cls(None, None) def clear_cofactor(self) -> BLS12_381_G2Point: return self * self.curve.params.cofactor @@ -197,7 +197,7 @@ def point_to_string(self) -> bytes: raise NotImplementedError("BLS12-381 G2 point serialization is not implemented") @classmethod - def string_to_point(cls, data: str | bytes, curve: SWCurve[Fp2]) -> BLS12_381_G2Point: + def string_to_point(cls, data: str | bytes) -> BLS12_381_G2Point: raise NotImplementedError("BLS12-381 G2 point deserialization is not implemented") def __add__(self, other: BLS12_381_G2Point) -> BLS12_381_G2Point: # type: ignore[override] @@ -219,7 +219,7 @@ def __add__(self, other: BLS12_381_G2Point) -> BLS12_381_G2Point: # type: ignor if other.is_identity(): return self - return self._from_py_ecc_point(add(self._py_ecc_point(), other._py_ecc_point()), self.curve) + return self._from_py_ecc_point(add(self._py_ecc_point(), other._py_ecc_point())) def __neg__(self) -> BLS12_381_G2Point: """ @@ -234,7 +234,7 @@ def __neg__(self) -> BLS12_381_G2Point: if self.x is None or self.y is None: raise ValueError("Invalid G2 point coordinate") - return self.__class__(self.x, -self.y, self.curve) + return self.__class__(self.x, -self.y) def __sub__(self, other: BLS12_381_G2Point) -> BLS12_381_G2Point: # type: ignore[override] """ @@ -263,39 +263,31 @@ def __mul__(self, scalar: int) -> BLS12_381_G2Point: BLS12_381_G2Point: The result of scalar multiplication """ if scalar == 0: - return self.identity(self.curve) + return self.identity() if scalar < 0: return (-self) * (-scalar) - return self._from_py_ecc_point(multiply(self._py_ecc_point(), scalar), self.curve) + return self._from_py_ecc_point(multiply(self._py_ecc_point(), scalar)) @classmethod def encode_to_curve( cls, - alpha_string: bytes | str, - salt: bytes | str = b"", - curve: SWCurve[Fp2] | None = None, + alpha_string: bytes, + salt: bytes = b"", ) -> BLS12_381_G2Point: - if curve is None: - raise ValueError("curve is required") - if not isinstance(alpha_string, bytes): - alpha_string = bytes.fromhex(alpha_string) - if not isinstance(salt, bytes): - salt = bytes.fromhex(salt) - - if curve.e2c_variant == E2C_Variant.SSWU_NU: - return cls._encode_sswu_nu(alpha_string, curve, salt) - if curve.e2c_variant == E2C_Variant.SSWU: - return cls._encode_sswu_ro(alpha_string, curve, salt) - raise ValueError(f"Unexpected E2C Variant: {curve.e2c_variant}") + if cls.curve.e2c_variant == E2C_Variant.SSWU_NU: + return cls._encode_sswu_nu(alpha_string, salt) + if cls.curve.e2c_variant == E2C_Variant.SSWU: + return cls._encode_sswu_ro(alpha_string, salt) + return super().encode_to_curve(alpha_string, salt) @classmethod def _encode_sswu_ro( cls, alpha_string: bytes, - curve: SWCurve[Fp2], salt: bytes = b"", ) -> Self: """Encode with the random-oracle simplified-SWU hash-to-curve variant.""" + curve = cls.curve string_to_hash = salt + alpha_string # Get field elements - this returns [re0, im0, re1, im1] @@ -305,9 +297,9 @@ def _encode_sswu_ro( Fp2(u_raw[2], u_raw[3], curve.params.field_modulus), ) - q0 = cls.map_to_curve_simple_swu(u[0], curve) + q0 = cls.map_to_curve_simple_swu(u[0]) - q1 = cls.map_to_curve_simple_swu(u[1], curve) + q1 = cls.map_to_curve_simple_swu(u[1]) R = q0 + q1 return cast(Self, R * curve.params.cofactor) @@ -316,30 +308,31 @@ def _encode_sswu_ro( def _encode_sswu_nu( cls, alpha_string: bytes, - curve: SWCurve[Fp2], salt: bytes = b"", ) -> Self: """Encode with the nonuniform simplified-SWU hash-to-curve variant.""" + curve = cls.curve string_to_hash = salt + alpha_string u_raw = curve.hash_to_field(string_to_hash, 1) # for nu u0 = Fp2(u_raw[0], u_raw[1], curve.params.field_modulus) - q0 = cls.map_to_curve_simple_swu(u0, curve) + q0 = cls.map_to_curve_simple_swu(u0) return cast(Self, q0 * curve.params.cofactor) @classmethod - def map_to_curve_simple_swu(cls, u: Fp2, curve: SWCurve) -> BLS12_381_G2Point: # type: ignore[override] + def map_to_curve_simple_swu(cls, u: Fp2) -> BLS12_381_G2Point: # type: ignore[override] """ Simplified SWU map with 3-isogeny for BLS12-381 G2 Combines SSWU map and 3-isogeny map in one function """ + curve = cls.curve # 1. Map to the isogenous curve E' - point_on_e_prime = cls._sswu_map_to_e_prime(u, curve) + point_on_e_prime = cls._sswu_map_to_e_prime(u) # 2. Apply 3-isogeny map from E' to E - x, y = cls._apply_3_isogeny(point_on_e_prime, curve) + x, y = cls._apply_3_isogeny(point_on_e_prime) - point = cls(x, y, curve) + point = cls(x, y) if not point.is_on_curve(): left = y * y right = x * x * x + cast(Fp2, curve.params.b) @@ -351,9 +344,9 @@ def map_to_curve_simple_swu(cls, u: Fp2, curve: SWCurve) -> BLS12_381_G2Point: def _sgn0(x: Fp2) -> int: return x.sgn0() - @staticmethod - def _require_isogeny(curve: SWCurve) -> RationalIsogeny[Fp2]: - isogeny = curve.params.hash_to_curve.isogeny + @classmethod + def _require_isogeny(cls) -> RationalIsogeny[Fp2]: + isogeny = cls.curve.params.hash_to_curve.isogeny if isogeny is None: raise ValueError("Missing isogeny") return cast(RationalIsogeny[Fp2], isogeny) @@ -366,8 +359,9 @@ def _evaluate_fp2_polynomial(cls, coefficients: tuple[Fp2, ...], x: Fp2) -> Fp2: return value @classmethod - def _sswu_map_to_e_prime(cls, u: Fp2, curve: SWCurve) -> tuple[Fp2, Fp2]: - isogeny = cls._require_isogeny(curve) + def _sswu_map_to_e_prime(cls, u: Fp2) -> tuple[Fp2, Fp2]: + curve = cls.curve + isogeny = cls._require_isogeny() Z = cast(Fp2, curve.params.hash_to_curve.z) A_prime = isogeny.map_curve.a B_prime = isogeny.map_curve.b @@ -406,9 +400,10 @@ def _sswu_map_to_e_prime(cls, u: Fp2, curve: SWCurve) -> tuple[Fp2, Fp2]: return x, cast(Fp2, y) @classmethod - def _apply_3_isogeny(cls, point: tuple[Fp2, Fp2], curve: SWCurve) -> tuple[Fp2, Fp2]: + def _apply_3_isogeny(cls, point: tuple[Fp2, Fp2]) -> tuple[Fp2, Fp2]: + curve = cls.curve x_prime, y_prime = point - isogeny = cls._require_isogeny(curve) + isogeny = cls._require_isogeny() x_num = cls._evaluate_fp2_polynomial(isogeny.x_numerator, x_prime) x_den = cls._evaluate_fp2_polynomial(isogeny.x_denominator, x_prime) @@ -425,14 +420,26 @@ def _apply_3_isogeny(cls, point: tuple[Fp2, Fp2], curve: SWCurve) -> tuple[Fp2, return x, y +BLS12_381_G2_NU_Curve = SWCurve(params=BLS12_381_G2_PARAMS, e2c_variant=E2C_Variant.SSWU_NU) +BLS12_381_G2_RO_Curve = SWCurve(params=BLS12_381_G2_PARAMS, e2c_variant=E2C_Variant.SSWU) + + +class BLS12_381_G2_NUPoint(BLS12_381_G2Point): + curve = BLS12_381_G2_NU_Curve + + +class BLS12_381_G2_ROPoint(BLS12_381_G2Point): + curve = BLS12_381_G2_RO_Curve + + BLS12_381_G2_NU = CurveVariant( name="BLS12_381_G2_NU", - curve=SWCurve(params=BLS12_381_G2_PARAMS, e2c_variant=E2C_Variant.SSWU_NU), - point_type=BLS12_381_G2Point, + curve=BLS12_381_G2_NU_Curve, + point_type=BLS12_381_G2_NUPoint, ) BLS12_381_G2_RO = CurveVariant( name="BLS12_381_G2_RO", - curve=SWCurve(params=BLS12_381_G2_PARAMS, e2c_variant=E2C_Variant.SSWU), - point_type=BLS12_381_G2Point, + curve=BLS12_381_G2_RO_Curve, + point_type=BLS12_381_G2_ROPoint, ) diff --git a/dot_ring/curve/specs/curve25519.py b/dot_ring/curve/specs/curve25519.py index b0c4618..43bf332 100644 --- a/dot_ring/curve/specs/curve25519.py +++ b/dot_ring/curve/specs/curve25519.py @@ -44,14 +44,26 @@ ) +Curve25519_NU_Curve = MGCurve(params=CURVE25519_PARAMS, e2c_variant=E2C_Variant.ELL2_NU) +Curve25519_RO_Curve = MGCurve(params=CURVE25519_PARAMS, e2c_variant=E2C_Variant.ELL2) + + +class Curve25519NUPoint(MGAffinePoint): + curve = Curve25519_NU_Curve + + +class Curve25519ROPoint(MGAffinePoint): + curve = Curve25519_RO_Curve + + Curve25519_NU = CurveVariant( name="Curve25519_NU", - curve=MGCurve(params=CURVE25519_PARAMS, e2c_variant=E2C_Variant.ELL2_NU), - point_type=MGAffinePoint, + curve=Curve25519_NU_Curve, + point_type=Curve25519NUPoint, ) Curve25519_RO = CurveVariant( name="Curve25519_RO", - curve=MGCurve(params=CURVE25519_PARAMS, e2c_variant=E2C_Variant.ELL2), - point_type=MGAffinePoint, + curve=Curve25519_RO_Curve, + point_type=Curve25519ROPoint, ) diff --git a/dot_ring/curve/specs/curve448.py b/dot_ring/curve/specs/curve448.py index 1318f3d..f18ca21 100644 --- a/dot_ring/curve/specs/curve448.py +++ b/dot_ring/curve/specs/curve448.py @@ -46,14 +46,26 @@ ) +Curve448_NU_Curve = MGCurve(params=CURVE448_PARAMS, e2c_variant=E2C_Variant.ELL2_NU) +Curve448_RO_Curve = MGCurve(params=CURVE448_PARAMS, e2c_variant=E2C_Variant.ELL2) + + +class Curve448NUPoint(MGAffinePoint): + curve = Curve448_NU_Curve + + +class Curve448ROPoint(MGAffinePoint): + curve = Curve448_RO_Curve + + Curve448_NU = CurveVariant( name="Curve448_NU", - curve=MGCurve(params=CURVE448_PARAMS, e2c_variant=E2C_Variant.ELL2_NU), - point_type=MGAffinePoint, + curve=Curve448_NU_Curve, + point_type=Curve448NUPoint, ) Curve448_RO = CurveVariant( name="Curve448_RO", - curve=MGCurve(params=CURVE448_PARAMS, e2c_variant=E2C_Variant.ELL2), - point_type=MGAffinePoint, + curve=Curve448_RO_Curve, + point_type=Curve448ROPoint, ) diff --git a/dot_ring/curve/specs/ed25519.py b/dot_ring/curve/specs/ed25519.py index 8ea7878..a7715c0 100644 --- a/dot_ring/curve/specs/ed25519.py +++ b/dot_ring/curve/specs/ed25519.py @@ -51,33 +51,50 @@ class Ed25519Point(TEAffinePoint[TECurve]): """Point on Ed25519.""" @classmethod - def map_to_curve(cls, u: int, curve: TECurve) -> Self: - s, t = curve.map_to_curve_ell2(u) - return cls.mont_to_ed25519(s, t, curve) + def map_to_curve(cls, u: int) -> Self: + s, t = cls.curve.map_to_curve_ell2(u) + return cls.mont_to_ed25519(s, t) @classmethod - def mont_to_ed25519(cls, u: int, v: int, curve: TECurve) -> Self: - p = curve.params.field_modulus - sqrt_neg_a_minus_2 = curve.mod_sqrt(-486664 % p) + def mont_to_ed25519(cls, u: int, v: int) -> Self: + p = cls.curve.params.field_modulus + sqrt_neg_a_minus_2 = cls.curve.mod_sqrt(-486664 % p) y = ((u - 1) * pow(u + 1, -1, p)) % p x = (sqrt_neg_a_minus_2 * u * pow(v, -1, p)) % p - return cls(x, y, curve) + return cls(x, y) + + +Ed25519_RO_Curve = TECurve(params=ED25519_PARAMS, e2c_variant=E2C_Variant.ELL2) +Ed25519_NU_Curve = TECurve(params=ED25519_PARAMS, e2c_variant=E2C_Variant.ELL2_NU) +Ed25519_TAI_Curve = TECurve(params=ED25519_PARAMS, e2c_variant=E2C_Variant.TAI) + + +class Ed25519ROPoint(Ed25519Point): + curve = Ed25519_RO_Curve + + +class Ed25519NUPoint(Ed25519Point): + curve = Ed25519_NU_Curve + + +class Ed25519TAIPoint(Ed25519Point): + curve = Ed25519_TAI_Curve Ed25519_RO = CurveVariant( name="Ed25519_RO", - curve=TECurve(params=ED25519_PARAMS, e2c_variant=E2C_Variant.ELL2), - point_type=Ed25519Point, + curve=Ed25519_RO_Curve, + point_type=Ed25519ROPoint, ) Ed25519_NU = CurveVariant( name="Ed25519_NU", - curve=TECurve(params=ED25519_PARAMS, e2c_variant=E2C_Variant.ELL2_NU), - point_type=Ed25519Point, + curve=Ed25519_NU_Curve, + point_type=Ed25519NUPoint, ) Ed25519_TAI = CurveVariant( name="Ed25519_TAI", - curve=TECurve(params=ED25519_PARAMS, e2c_variant=E2C_Variant.TAI), - point_type=Ed25519Point, + curve=Ed25519_TAI_Curve, + point_type=Ed25519TAIPoint, ) diff --git a/dot_ring/curve/specs/ed448.py b/dot_ring/curve/specs/ed448.py index 3f8bc7b..db3f485 100644 --- a/dot_ring/curve/specs/ed448.py +++ b/dot_ring/curve/specs/ed448.py @@ -53,17 +53,18 @@ class Ed448Point(TEAffinePoint[TECurve]): """Point on Ed448.""" @classmethod - def blinding_base(cls, curve: TECurve) -> Self: - x, y = curve.params.auxiliary_points.blinding_base or curve.params.generator - return cls(x, y, curve) + def blinding_base(cls) -> Self: + x, y = cls.curve.params.auxiliary_points.blinding_base or cls.curve.params.generator + return cls(x, y) @classmethod - def map_to_curve(cls, u: int, curve: TECurve) -> Self: - s, t = curve.map_to_curve_ell2(u) - return cls.mont_to_ed448(s, t, curve) + def map_to_curve(cls, u: int) -> Self: + s, t = cls.curve.map_to_curve_ell2(u) + return cls.mont_to_ed448(s, t) @classmethod - def mont_to_ed448(cls, u: int, v: int, curve: TECurve) -> Self: + def mont_to_ed448(cls, u: int, v: int) -> Self: + curve = cls.curve p = curve.params.field_modulus x_num = (4 * v * ((u * u - 1) % p)) % p x_den = (pow(u, 4, p) - 2 * pow(u, 2, p) + 4 * pow(v, 2, p) + 1) % p @@ -72,17 +73,29 @@ def mont_to_ed448(cls, u: int, v: int, curve: TECurve) -> Self: y_num = -(pow(u, 5, p) - 2 * pow(u, 3, p) - 4 * u * pow(v, 2, p) + u) % p y_den = (pow(u, 5, p) - 2 * pow(u, 2, p) * pow(v, 2, p) - 2 * pow(u, 3, p) - 2 * pow(v, 2, p) + u) % p y = (y_num * curve.inv(y_den)) % p - return cls(x, y, curve) + return cls(x, y) + + +Ed448_NU_Curve = TECurve(params=ED448_PARAMS, e2c_variant=E2C_Variant.ELL2_NU) +Ed448_RO_Curve = TECurve(params=ED448_PARAMS, e2c_variant=E2C_Variant.ELL2) + + +class Ed448NUPoint(Ed448Point): + curve = Ed448_NU_Curve + + +class Ed448ROPoint(Ed448Point): + curve = Ed448_RO_Curve Ed448_NU = CurveVariant( name="Ed448_NU", - curve=TECurve(params=ED448_PARAMS, e2c_variant=E2C_Variant.ELL2_NU), - point_type=Ed448Point, + curve=Ed448_NU_Curve, + point_type=Ed448NUPoint, ) Ed448_RO = CurveVariant( name="Ed448_RO", - curve=TECurve(params=ED448_PARAMS, e2c_variant=E2C_Variant.ELL2), - point_type=Ed448Point, + curve=Ed448_RO_Curve, + point_type=Ed448ROPoint, ) diff --git a/dot_ring/curve/specs/jubjub.py b/dot_ring/curve/specs/jubjub.py index 4ffa8db..353d237 100644 --- a/dot_ring/curve/specs/jubjub.py +++ b/dot_ring/curve/specs/jubjub.py @@ -52,8 +52,15 @@ ) +JubJub_Curve = TECurve(params=JUBJUB_PARAMS, e2c_variant=E2C_Variant.TAI) + + +class JubJubPoint(TEAffinePoint): + curve = JubJub_Curve + + JubJub = CurveVariant( name="JubJub", - curve=TECurve(params=JUBJUB_PARAMS, e2c_variant=E2C_Variant.TAI), - point_type=TEAffinePoint, + curve=JubJub_Curve, + point_type=JubJubPoint, ) diff --git a/dot_ring/curve/specs/p256.py b/dot_ring/curve/specs/p256.py index b1ee70e..406d8a6 100644 --- a/dot_ring/curve/specs/p256.py +++ b/dot_ring/curve/specs/p256.py @@ -53,7 +53,7 @@ class P256Point(SWAffinePoint): def point_to_string(self, compressed: bool = True) -> bytes: if self.curve.e2c_variant != E2C_Variant.TAI: - return super().point_to_string(compressed) + return super().point_to_string() if self.x is None and self.y is None: return bytes([0] * 32 + [0x40]) @@ -64,26 +64,28 @@ def point_to_string(self, compressed: bool = True) -> bytes: return x_bytes + bytes([flag]) @classmethod - def string_to_point(cls, data: str | bytes, curve: SWCurve): + def string_to_point(cls, data: str | bytes): + curve = cls.curve if isinstance(data, str): data = bytes.fromhex(data) if curve.e2c_variant != E2C_Variant.TAI: - return super().string_to_point(data, curve) + return super().string_to_point(data) elif len(data) == 33 and data[0] in (0x02, 0x03): # Canonical SW encodings put flags in the final byte, so external # vectors can coincidentally start with SEC1 marker bytes. try: - return cls._string_to_canonical_point(data, curve) + return cls._string_to_canonical_point(data) except ValueError: - return super().string_to_point(data, curve) + return super().string_to_point(data) if len(data) != 33: raise ValueError(f"Invalid compressed point length: expected 33, got {len(data)}") - return cls._string_to_canonical_point(data, curve) + return cls._string_to_canonical_point(data) @classmethod - def _string_to_canonical_point(cls, data: bytes, curve: SWCurve): + def _string_to_canonical_point(cls, data: bytes): + curve = cls.curve flag = data[-1] is_negative = (flag >> 7) & 1 is_infinity = (flag >> 6) & 1 @@ -92,19 +94,20 @@ def _string_to_canonical_point(cls, data: bytes, curve: SWCurve): if is_infinity: if is_negative or any(data[:-1]): raise ValueError("Invalid infinity encoding") - return cls.identity(curve) + return cls.identity() x = int.from_bytes(data[:-1], "little") if x >= curve.params.field_modulus: raise ValueError("x-coordinate is not in field") - y_candidates = cls._y_recover(x, curve) + y_candidates = cls._y_recover(x) if y_candidates is None: raise ValueError("Invalid point") y, y_neg = y_candidates - return cls(x, y_neg if is_negative else y, curve) + return cls(x, y_neg if is_negative else y) @classmethod - def _y_recover(cls, x: int, curve: SWCurve) -> tuple[int, int] | None: + def _y_recover(cls, x: int) -> tuple[int, int] | None: + curve = cls.curve p = curve.params.field_modulus y_square = (pow(x, 3, p) + curve.params.a * x + curve.params.b) % p try: @@ -115,20 +118,37 @@ def _y_recover(cls, x: int, curve: SWCurve) -> tuple[int, int] | None: return (y, neg_y) if y <= neg_y else (neg_y, y) +P256_RO_Curve = SWCurve(params=P256_PARAMS, e2c_variant=E2C_Variant.SSWU) +P256_NU_Curve = SWCurve(params=P256_PARAMS, e2c_variant=E2C_Variant.SSWU_NU) +P256_TAI_Curve = SWCurve(params=P256_PARAMS, e2c_variant=E2C_Variant.TAI) + + +class P256ROPoint(P256Point): + curve = P256_RO_Curve + + +class P256NUPoint(P256Point): + curve = P256_NU_Curve + + +class P256TAIPoint(P256Point): + curve = P256_TAI_Curve + + P256_RO = CurveVariant( name="P256_RO", - curve=SWCurve(params=P256_PARAMS, e2c_variant=E2C_Variant.SSWU), - point_type=P256Point, + curve=P256_RO_Curve, + point_type=P256ROPoint, ) P256_NU = CurveVariant( name="P256_NU", - curve=SWCurve(params=P256_PARAMS, e2c_variant=E2C_Variant.SSWU_NU), - point_type=P256Point, + curve=P256_NU_Curve, + point_type=P256NUPoint, ) P256_TAI = CurveVariant( name="P256_TAI", - curve=SWCurve(params=P256_PARAMS, e2c_variant=E2C_Variant.TAI), - point_type=P256Point, + curve=P256_TAI_Curve, + point_type=P256TAIPoint, ) diff --git a/dot_ring/curve/specs/p384.py b/dot_ring/curve/specs/p384.py index 019b085..37f6a81 100644 --- a/dot_ring/curve/specs/p384.py +++ b/dot_ring/curve/specs/p384.py @@ -44,14 +44,26 @@ ) +P384_RO_Curve = SWCurve(params=P384_PARAMS, e2c_variant=E2C_Variant.SSWU) +P384_NU_Curve = SWCurve(params=P384_PARAMS, e2c_variant=E2C_Variant.SSWU_NU) + + +class P384ROPoint(SWAffinePoint): + curve = P384_RO_Curve + + +class P384NUPoint(SWAffinePoint): + curve = P384_NU_Curve + + P384_RO = CurveVariant( name="P384_RO", - curve=SWCurve(params=P384_PARAMS, e2c_variant=E2C_Variant.SSWU), - point_type=SWAffinePoint, + curve=P384_RO_Curve, + point_type=P384ROPoint, ) P384_NU = CurveVariant( name="P384_NU", - curve=SWCurve(params=P384_PARAMS, e2c_variant=E2C_Variant.SSWU_NU), - point_type=SWAffinePoint, + curve=P384_NU_Curve, + point_type=P384NUPoint, ) diff --git a/dot_ring/curve/specs/p521.py b/dot_ring/curve/specs/p521.py index fe47307..dec7373 100644 --- a/dot_ring/curve/specs/p521.py +++ b/dot_ring/curve/specs/p521.py @@ -44,14 +44,26 @@ ) +P521_RO_Curve = SWCurve(params=P521_PARAMS, e2c_variant=E2C_Variant.SSWU) +P521_NU_Curve = SWCurve(params=P521_PARAMS, e2c_variant=E2C_Variant.SSWU_NU) + + +class P521ROPoint(SWAffinePoint): + curve = P521_RO_Curve + + +class P521NUPoint(SWAffinePoint): + curve = P521_NU_Curve + + P521_RO = CurveVariant( name="P521_RO", - curve=SWCurve(params=P521_PARAMS, e2c_variant=E2C_Variant.SSWU), - point_type=SWAffinePoint, + curve=P521_RO_Curve, + point_type=P521ROPoint, ) P521_NU = CurveVariant( name="P521_NU", - curve=SWCurve(params=P521_PARAMS, e2c_variant=E2C_Variant.SSWU_NU), - point_type=SWAffinePoint, + curve=P521_NU_Curve, + point_type=P521NUPoint, ) diff --git a/dot_ring/curve/specs/secp256k1.py b/dot_ring/curve/specs/secp256k1.py index 8303c12..6687669 100644 --- a/dot_ring/curve/specs/secp256k1.py +++ b/dot_ring/curve/specs/secp256k1.py @@ -84,7 +84,7 @@ class Secp256k1Point(SWAffinePoint): def __mul__(self, scalar: int) -> Self: if scalar == 0: - return cast(Self, self.identity(self.curve)) + return cast(Self, self.identity()) if scalar < 0: return cast(Self, (-self).__mul__(-scalar)) @@ -93,14 +93,26 @@ def __mul__(self, scalar: int) -> Self: return cast(Self, super().__mul__(scalar)) +Secp256k1_RO_Curve = SWCurve(params=SECP256K1_PARAMS, e2c_variant=E2C_Variant.SSWU) +Secp256k1_NU_Curve = SWCurve(params=SECP256K1_PARAMS, e2c_variant=E2C_Variant.SSWU_NU) + + +class Secp256k1ROPoint(Secp256k1Point): + curve = Secp256k1_RO_Curve + + +class Secp256k1NUPoint(Secp256k1Point): + curve = Secp256k1_NU_Curve + + Secp256k1_RO = CurveVariant( name="Secp256k1_RO", - curve=SWCurve(params=SECP256K1_PARAMS, e2c_variant=E2C_Variant.SSWU), - point_type=Secp256k1Point, + curve=Secp256k1_RO_Curve, + point_type=Secp256k1ROPoint, ) Secp256k1_NU = CurveVariant( name="Secp256k1_NU", - curve=SWCurve(params=SECP256K1_PARAMS, e2c_variant=E2C_Variant.SSWU_NU), - point_type=Secp256k1Point, + curve=Secp256k1_NU_Curve, + point_type=Secp256k1NUPoint, ) diff --git a/dot_ring/curve/twisted_edwards/te_affine_point.py b/dot_ring/curve/twisted_edwards/te_affine_point.py index 0068a42..0e42171 100644 --- a/dot_ring/curve/twisted_edwards/te_affine_point.py +++ b/dot_ring/curve/twisted_edwards/te_affine_point.py @@ -42,14 +42,14 @@ def is_identity(self) -> bool: return self.x == 0 and self.y == 1 @classmethod - def identity(cls, curve: C) -> Self: + def identity(cls) -> Self: """ Get the Twisted Edwards identity element. Returns: TEAffinePoint: Identity element """ - return cls(0, 1, curve) + return cls(0, 1) def is_on_curve(self) -> bool: """ @@ -111,7 +111,7 @@ def __add__(self, other: CurvePoint[C, int]) -> Self: x3 = ((x1y2 + x2y1) * _invert(1 + dx1x2y1y2, p)) % p y3 = ((y1y2 - a_coeff * x1x2) * _invert(1 - dx1x2y1y2, p)) % p - return self.__class__(int(x3), int(y3), self.curve) + return self.__class__(int(x3), int(y3)) def __neg__(self) -> Self: """ @@ -121,7 +121,7 @@ def __neg__(self) -> Self: TEAffinePoint: Negated point """ x = -cast(int, self.x) % self.curve.params.field_modulus - return self.__class__(x, self.y, self.curve) + return self.__class__(x, self.y) def __sub__(self, other: CurvePoint[C, int]) -> Self: """ @@ -147,18 +147,18 @@ def double(self) -> Self: x1, y1 = _mpz(cast(int, self.x)) % p, _mpz(cast(int, self.y)) % p if y1 == 0: - return self.identity(self.curve) + return self.identity() # Specialized affine doubling formulas for twisted Edwards curves. denom_x = (a_coeff * x1**2 + y1**2) % p denom_y = (2 - a_coeff * x1**2 - y1**2) % p if denom_x == 0 or denom_y == 0: - return self.identity(self.curve) + return self.identity() x3 = (2 * x1 * y1 * _invert(denom_x, p)) % p y3 = ((y1**2 - a_coeff * x1**2) * _invert(denom_y, p)) % p - return self.__class__(int(x3), int(y3), self.curve) + return self.__class__(int(x3), int(y3)) def __mul__(self, scalar: int) -> Self: """ @@ -194,58 +194,43 @@ def __rmul__(self, scalar: int) -> Self: @classmethod def encode_to_curve( cls, - alpha_string: bytes | str, - salt: bytes | str = b"", - curve: C | None = None, + alpha_string: bytes, + salt: bytes = b"", ) -> Self: """ Encode input bytes to a point using the curve's configured hash-to-curve variant. """ - if curve is None: - raise ValueError("curve is required") - if not isinstance(alpha_string, bytes): - alpha_string = bytes.fromhex(alpha_string) - - if not isinstance(salt, bytes): - salt = bytes.fromhex(salt) - - if curve.e2c_variant == E2C_Variant.TAI: - from dot_ring.vrf.transcript import hash_to_curve_tai - - return cast(Self, hash_to_curve_tai(cls, salt + alpha_string, curve)) - - if curve.e2c_variant in (E2C_Variant.ELL2, E2C_Variant.ELL2_NU): - if curve.e2c_variant.value.endswith("_NU_"): - return cls._encode_elligator2_nu(alpha_string, curve, salt) - return cls._encode_elligator2_ro(alpha_string, curve, salt) - - raise ValueError("Unexpected E2C Variant") + match cls.curve.e2c_variant: + case E2C_Variant.ELL2_NU: + return cls._e2c_ell2_nu(alpha_string, salt) + case E2C_Variant.ELL2: + return cls._e2c_ell2_ro(alpha_string, salt) + case _: + return super().encode_to_curve(alpha_string, salt) @classmethod - def _encode_elligator2_ro( + def _e2c_ell2_ro( cls, alpha_string: bytes, - curve: C, salt: bytes = b"", ) -> Self: """Encode with the random-oracle Elligator2 hash-to-curve variant.""" string_to_hash = salt + alpha_string - u0, u1 = curve.hash_to_field(string_to_hash, 2) - q0 = cls.map_to_curve(u0, curve) - q1 = cls.map_to_curve(u1, curve) + u0, u1 = cls.curve.hash_to_field(string_to_hash, 2) + q0 = cls.map_to_curve(u0) + q1 = cls.map_to_curve(u1) return (q0 + q1).clear_cofactor() @classmethod - def _encode_elligator2_nu( + def _e2c_ell2_nu( cls, alpha_string: bytes, - curve: C, salt: bytes = b"", ) -> Self: """Encode with the nonuniform Elligator2 hash-to-curve variant.""" string_to_hash = salt + alpha_string - (u0,) = curve.hash_to_field(string_to_hash, 1) - return cls.map_to_curve(u0, curve).clear_cofactor() + (u0,) = cls.curve.hash_to_field(string_to_hash, 1) + return cls.map_to_curve(u0).clear_cofactor() def clear_cofactor(self) -> Self: """ @@ -265,7 +250,7 @@ def clear_cofactor(self) -> Self: return self * cofactor @classmethod - def map_to_curve(cls, u: int, curve: C) -> Self: + def map_to_curve(cls, u: int) -> Self: """ Map a field element to a curve point using Elligator 2. @@ -276,11 +261,11 @@ def map_to_curve(cls, u: int, curve: C) -> Self: TEAffinePoint: Resulting curve point """ - s, t = curve.map_to_curve_ell2(u) - return cls.from_mont(s, t, curve) + s, t = cls.curve.map_to_curve_ell2(u) + return cls.from_mont(s, t) @classmethod - def from_mont(cls, s: int, t: int, curve: C) -> Self: + def from_mont(cls, s: int, t: int) -> Self: """ Convert from Montgomery to Twisted Edwards coordinates. @@ -291,7 +276,7 @@ def from_mont(cls, s: int, t: int, curve: C) -> Self: Returns: TEAffinePoint: Point in Twisted Edwards coordinates """ - field = curve.params.field_modulus + field = cls.curve.params.field_modulus tv1 = (s + 1) % field tv2 = (tv1 * t) % field @@ -306,11 +291,12 @@ def from_mont(cls, s: int, t: int, curve: C) -> Self: # Handle exceptional case w = 1 if tv2 == 0 else w - return cls(v, w, curve) + return cls(v, w) @classmethod - def _x_recover(cls, y: int, curve: C) -> int | tuple[int, int] | None: + def _x_recover(cls, y: int) -> int | tuple[int, int] | None: """Recover the possible x-coordinates for a compressed Edwards y-coordinate.""" + curve = cls.curve p = curve.params.field_modulus lhs = (1 - y * y) % p rhs = (curve.params.a - curve.params.d * y * y) % p diff --git a/dot_ring/curve/twisted_edwards/te_projective_point.py b/dot_ring/curve/twisted_edwards/te_projective_point.py index e3dbdec..6b53c61 100644 --- a/dot_ring/curve/twisted_edwards/te_projective_point.py +++ b/dot_ring/curve/twisted_edwards/te_projective_point.py @@ -44,13 +44,13 @@ def to_affine(self, point_type: type[TEAffinePoint] | None = None) -> TEAffinePo target_type = point_type or BaseAffine if self.z == 0: - return target_type(0, 1, self.curve) + return target_type(0, 1) p = self.curve.params.field_modulus inv_z = pow(self.z, -1, p) x = (self.x * inv_z) % p y = (self.y * inv_z) % p - return target_type(x, y, self.curve) + return target_type(x, y) @classmethod def zero(cls, curve: C) -> Self: diff --git a/dot_ring/ring_proof/columns/columns.py b/dot_ring/ring_proof/columns/columns.py index aaeafa0..e7bf9bc 100644 --- a/dot_ring/ring_proof/columns/columns.py +++ b/dot_ring/ring_proof/columns/columns.py @@ -133,7 +133,7 @@ def _conditional_sum_accumulator(self, b_vector: list[int]) -> tuple[list[int], else: current = acc[i - 1] member = self.ring_pk[i - 1] - added = self.params.cv.point(current[0], current[1]) + self.params.cv.point(member[0], member[1]) + added = self.params.cv.point_type(*current) + self.params.cv.point_type(*member) next_pt = int(added.x), int(added.y) acc.append(next_pt) return [point[0] for point in acc], [point[1] for point in acc] @@ -162,6 +162,6 @@ def build(self) -> tuple[Column, Column, Column, Column]: return (columns[0], columns[1], columns[2], columns[3]) def result(self, blinding_point: tuple[int, int]) -> CurvePoint: - producer_point = self.params.cv.point(self.ring_pk[self.producer_index]) - blinded = self.params.cv.point(blinding_point) * self.secret_t + producer_point = self.params.cv.point_type(*self.ring_pk[self.producer_index]) + blinded = self.params.cv.point_type(*blinding_point) * self.secret_t return producer_point + blinded diff --git a/dot_ring/ring_proof/polynomial/fft.py b/dot_ring/ring_proof/polynomial/fft.py index 3d0c6d5..b569cb8 100644 --- a/dot_ring/ring_proof/polynomial/fft.py +++ b/dot_ring/ring_proof/polynomial/fft.py @@ -7,7 +7,6 @@ from functools import lru_cache -from dot_ring.curve.specs.bandersnatch import BANDERSNATCH_FIELD_MODULUS from dot_ring.ring_proof.polynomial.ntt import BlsScalarNTTPlan @@ -56,8 +55,6 @@ def _get_twiddle_factors(n: int, omega: int, prime: int) -> list[list[int]]: return twiddles - - def _fft_in_place(coeffs: list[int], omega: int, prime: int) -> None: """In-place Cooley-Tukey. @@ -69,7 +66,7 @@ def _fft_in_place(coeffs: list[int], omega: int, prime: int) -> None: n = len(coeffs) if n == 1: return - + BlsScalarNTTPlan(_get_twiddle_factors(n, omega, prime), _get_bit_reverse(n)).transform(coeffs) diff --git a/dot_ring/vrf/ring/proof_builder.py b/dot_ring/ring_proof/proof_builder.py similarity index 96% rename from dot_ring/vrf/ring/proof_builder.py rename to dot_ring/ring_proof/proof_builder.py index 3da6a69..5808832 100644 --- a/dot_ring/vrf/ring/proof_builder.py +++ b/dot_ring/ring_proof/proof_builder.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Sequence +from typing import Any from dot_ring.curve.curve import CurveVariant from dot_ring.curve.point import CurvePoint @@ -10,13 +11,10 @@ from dot_ring.ring_proof.constraints.constraints import RingConstraintBuilder from dot_ring.ring_proof.polynomial.fft import inverse_fft from dot_ring.ring_proof.polynomial.ops import poly_add, poly_divide_by_vanishing, poly_evaluate_single, poly_multiply, poly_scalar_mul +from dot_ring.ring_proof.proof_payload import RingProofPayload from dot_ring.ring_proof.transcript.phases import phase1_alphas_after_vk, phase2_eval_point, phase3_nu_vector from dot_ring.ring_proof.transcript.transcript import FiatShamirTranscript -from .members import Ring -from .proof_payload import RingProofPayload -from .root import RingRoot - class RingProofBuilder: """Build the SNARK payload that proves `producer_key + b*B` is in the committed ring.""" @@ -26,9 +24,9 @@ def __init__( curve: CurveVariant, blinding_factor: int, producer_key: bytes | str, - ring: Ring, + ring: Any, transcript_challenge: bytes | None = None, - ring_root: RingRoot | None = None, + ring_root: Any | None = None, ) -> None: self.curve = curve self.blinding_factor = blinding_factor @@ -40,7 +38,12 @@ def __init__( def build(self) -> RingProofPayload: """Spec sections 3.1-3.3 in order: witness, constraints, quotient, linearization, openings.""" params = self.ring.params - ring_root = self.ring_root or RingRoot.from_ring(self.ring, params) + if self.ring_root is None: + from dot_ring.vrf.ring.root import RingRoot + + ring_root = RingRoot.from_ring(self.ring, params) + else: + ring_root = self.ring_root producer_index = self.ring.index_of(self.producer_key) # Section 3.1: witness bit column and accumulators for R = PK_k + bB. @@ -54,7 +57,7 @@ def build(self) -> RingProofPayload: witness_columns = witness_builder.build() relation_point = witness_builder.result(params.cv.curve.params.auxiliary_points.blinding_base) seed_point = params.cv.curve.params.auxiliary_points.accumulator_base - relation_plus_seed_point = relation_point + params.cv.point(seed_point) + relation_plus_seed_point = relation_point + params.cv.point_type(*seed_point) relation_plus_seed = int(relation_plus_seed_point.x), int(relation_plus_seed_point.y) # Section 3.2: c1-c7 constraints over fixed and witness columns. @@ -132,7 +135,7 @@ def build(self) -> RingProofPayload: def _phase1_transcript( self, - ring_root: RingRoot, + ring_root: Any, relation_point: CurvePoint, witness_columns: tuple[Column, Column, Column, Column], ) -> tuple[FiatShamirTranscript, list[int]]: diff --git a/dot_ring/vrf/ring/proof_payload.py b/dot_ring/ring_proof/proof_payload.py similarity index 99% rename from dot_ring/vrf/ring/proof_payload.py rename to dot_ring/ring_proof/proof_payload.py index c1eab0d..6ea9328 100644 --- a/dot_ring/vrf/ring/proof_payload.py +++ b/dot_ring/ring_proof/proof_payload.py @@ -8,6 +8,7 @@ RING_SCALAR_LEN = 32 + @dataclass(slots=True) class RingProofPayload: c_b: Column diff --git a/dot_ring/ring_proof/ring_curve.py b/dot_ring/ring_proof/ring_curve.py index a676fd3..1e68fac 100644 --- a/dot_ring/ring_proof/ring_curve.py +++ b/dot_ring/ring_proof/ring_curve.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, cast +from typing import cast from dot_ring.curve.curve import CurveVariant from dot_ring.curve.point import CurvePoint @@ -37,6 +37,13 @@ def ring_coords(public_curve: CurveVariant, point: CurvePoint | tuple[int, int]) return coords +def ring_auxiliary_point(public_curve: CurveVariant, name: str) -> tuple[int, int]: + curve = ring_proof_curve(public_curve) + point = getattr(curve.curve.params.auxiliary_points, name) + if point is None: + raise ValueError(f"{public_curve.name} ring proofs require auxiliary point {name}") + return int(point[0]), int(point[1]) + def validate_ring_curve(public_curve: CurveVariant, prime: int) -> None: curve = ring_proof_curve(public_curve) diff --git a/dot_ring/scripts/export_python_proof.py b/dot_ring/scripts/export_python_proof.py deleted file mode 100644 index 6f888ab..0000000 --- a/dot_ring/scripts/export_python_proof.py +++ /dev/null @@ -1,337 +0,0 @@ -""" -Export Python-generated ring proofs to canonical JSON. - -This script generates multiple ring proof variants using the Python implementation and -exports them in a Rust-verifier-compatible proof-vector format. -""" - -from __future__ import annotations - -import json -import sys -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -# Add project root to path to import dot_ring modules -sys.path.insert(0, str(Path(__file__).parent.parent.parent)) - -from py_ecc.optimized_bls12_381 import normalize as nm - -from dot_ring.curve.specs.bandersnatch import Bandersnatch -from dot_ring.ring_proof.params import RingProofParams -from dot_ring.ring_proof.pcs.srs import srs -from dot_ring.vrf.ring import Ring, RingRoot -from dot_ring.vrf.ring.proof_builder import RingProofBuilder -from tests.utils.python_to_rust_serde import ( - serialize_bls12_381_g1, - serialize_bls12_381_g2, - serialize_fq_field_element, - serialize_ring_proof, -) - -Blinding_Base = ( - int.from_bytes(bytes.fromhex("e8c5e337ffbd7839ed5aaee576faae32eea01bff684125758d874fa909e8980d"), "little"), - int.from_bytes(bytes.fromhex("e93da06b869766b158d20b843ec648cc68e0b7ba2f7083acf0f154205d04e23e"), "little"), -) -SeedPoint = ( - int.from_bytes(bytes.fromhex("20f354ea2af5f890e0cfac3b044aca2335fc26fa900fbe429fb059b0df319553"), "little"), - int.from_bytes(bytes.fromhex("6e5574f9077fb76c885c36196a832dbadd64142d305be5487724967acf959520"), "little"), -) - -blinding_factor = int.from_bytes(bytes.fromhex("2e98974f0b99a70d4fbe7c1ea62a5ada75c899deb30e9d27f9e5da79177c0619"), "big") - - -def _as_bandersnatch_point(point: tuple[int | None, int | None]): - x, y = point - if x is None or y is None: - return Bandersnatch.identity() - return Bandersnatch.point(int(x), int(y)) - - -def _bandersnatch_mul(k: int, point: tuple[int | None, int | None]) -> tuple[int | None, int | None]: - if point[0] is None or point[1] is None: - result = Bandersnatch.identity() - else: - result = _as_bandersnatch_point(point) * k - return result.x, result.y - - -def _bandersnatch_add( - point1: tuple[int | None, int | None], - point2: tuple[int | None, int | None], -) -> tuple[int | None, int | None]: - result = _as_bandersnatch_point(point1) + _as_bandersnatch_point(point2) - return result.x, result.y - - -@dataclass(frozen=True) -class VariantSpec: - name: str - domain_size: int - ring_size: int - padding_rows: int = 4 - radix_domain_size: int | None = None - prover_index: int = 42 - - -DEFAULT_VARIANTS = [ - VariantSpec( - name="ring_proof_ring64_domain512.json", - domain_size=512, - ring_size=64, - ), - VariantSpec( - name="ring_proof_ring128_domain512.json", - domain_size=512, - ring_size=128, - ), - VariantSpec( - name="ring_proof_ring256_domain1024.json", - domain_size=1024, - ring_size=256, - ), - VariantSpec( - name="ring_proof_ring1024_domain2048.json", - domain_size=2048, - ring_size=1000, - ), -] - - -def generate_test_keys( - num_keys: int, - prover_index: int, -) -> tuple[list[bytes], list[tuple[int, int]], int]: - """ - Generate deterministic, valid Bandersnatch public keys. - - Args: - num_keys: Number of keys in the ring - prover_index: Index of the prover's key - - Returns: - Tuple of (list of compressed key bytes, list of key points, prover index) - """ - if num_keys < 1: - raise ValueError("num_keys must be >= 1") - if prover_index >= num_keys: - raise ValueError("prover_index must be < num_keys") - - base = Bandersnatch.generator_point() - - keys_bytes: list[bytes] = [] - keys_points: list[tuple[int, int]] = [] - for i in range(1, num_keys + 1): - pt = base * i - keys_points.append((int(pt.x), int(pt.y))) - keys_bytes.append(pt.point_to_string()) - - return keys_bytes, keys_points, prover_index - - -def export_variant(variant: VariantSpec, output_dir: Path) -> dict[str, Any]: - """Generate and export a single proof variant.""" - params = RingProofParams( - domain_size=variant.domain_size, - max_ring_size=variant.ring_size, - padding_rows=variant.padding_rows, - radix_domain_size=variant.radix_domain_size, - ) - - if variant.ring_size > params.max_effective_ring_size: - raise ValueError( - f"ring_size {variant.ring_size} exceeds max supported size {params.max_effective_ring_size} " - f"for domain {variant.domain_size} with padding_rows={variant.padding_rows}" - ) - - # Deterministic test parameters - secret_bits = max(1, blinding_factor.bit_length()) - max_secret_bits = params.domain_size - params.padding_rows - params.max_ring_size - if max_secret_bits < 1: - raise ValueError( - "ring_size too large for any secret_t bits: " - f"ring_size={params.max_ring_size}, domain_size={params.domain_size}, padding_rows={params.padding_rows}" - ) - if secret_bits > max_secret_bits: - raise ValueError( - "secret_t bit length exceeds available rows: " - f"{secret_bits} > {max_secret_bits} (ring_size={params.max_ring_size}, " - f"domain_size={params.domain_size}, padding_rows={params.padding_rows})" - ) - prover_index = min(variant.prover_index, variant.ring_size - 1) - keys_bytes, keys_points, prover_index = generate_test_keys(num_keys=variant.ring_size, prover_index=prover_index) - - # Build ring and ring root - ring = Ring(keys_bytes, params) - ring_root = RingRoot.from_ring(ring, params) - - # Producer key - producer_key_bytes = keys_bytes[prover_index] - producer_key_point = keys_points[prover_index] - - # Generate ring proof using a Rust-test-compatible transcript label - proof_components = ( - RingProofBuilder( - curve=Bandersnatch, - blinding_factor=blinding_factor, - producer_key=producer_key_bytes, - ring=ring, - transcript_challenge=b"w3f-ring-proof-test", - ring_root=ring_root, - ) - .build() - .as_tuple() - ) - - # Compute result point (blinded public key) - result_point = _bandersnatch_mul(blinding_factor, Blinding_Base) - result_point = _bandersnatch_add(result_point, producer_key_point) - - # Unpack proof components - ( - c_b, - c_acc_ip, - c_acc_x, - c_acc_y, - p_x_zeta, - p_y_zeta, - s_zeta, - b_zeta, - acc_ip_zeta, - acc_x_zeta, - acc_y_zeta, - c_q, - l_zeta_omega, - phi_z, - phi_zw, - ) = proof_components - - # Normalize commitments to affine coordinates - c_b_affine = nm(c_b.commitment) - c_acc_ip_affine = nm(c_acc_ip.commitment) - c_acc_x_affine = nm(c_acc_x.commitment) - c_acc_y_affine = nm(c_acc_y.commitment) - c_q_affine = nm(c_q.commitment) - phi_z_affine = nm(phi_z.proof) - phi_zw_affine = nm(phi_zw.proof) - - # Serialize proof bytes (for size/debug) - proof_bytes = serialize_ring_proof( - column_commitments=[c_b_affine, c_acc_ip_affine, c_acc_x_affine, c_acc_y_affine], - columns_at_zeta=[p_x_zeta, p_y_zeta, s_zeta, b_zeta, acc_ip_zeta, acc_x_zeta, acc_y_zeta], - quotient_commitment=c_q_affine, - lin_at_zeta_omega=l_zeta_omega, - agg_at_zeta_proof=phi_z_affine, - lin_at_zeta_omega_proof=phi_zw_affine, - ) - - # Serialize first 3 SRS points for verifier key - srs_g1_0 = serialize_bls12_381_g1(srs.g1_points[0]) - srs_g2_0 = serialize_bls12_381_g2(srs.g2_points[0]) - srs_g2_1 = serialize_bls12_381_g2(srs.g2_points[1]) - - # Serialize fixed column commitments - fixed_cols_cmts_affine = [ - nm(ring_root.px.commitment), - nm(ring_root.py.commitment), - nm(ring_root.s.commitment), - ] - - verifier_key_bytes = bytearray() - verifier_key_bytes.extend(srs_g1_0) - verifier_key_bytes.extend(srs_g2_0) - verifier_key_bytes.extend(srs_g2_1) - for commitment in fixed_cols_cmts_affine: - verifier_key_bytes.extend(serialize_bls12_381_g1(commitment)) - - # Concatenate column commitments (4 G1 points) - column_commitments_concat = ( - serialize_bls12_381_g1(c_b_affine) - + serialize_bls12_381_g1(c_acc_ip_affine) - + serialize_bls12_381_g1(c_acc_x_affine) - + serialize_bls12_381_g1(c_acc_y_affine) - ).hex() - - # Concatenate columns at zeta (7 field elements) - columns_at_zeta_concat = ( - serialize_fq_field_element(p_x_zeta) - + serialize_fq_field_element(p_y_zeta) - + serialize_fq_field_element(s_zeta) - + serialize_fq_field_element(b_zeta) - + serialize_fq_field_element(acc_ip_zeta) - + serialize_fq_field_element(acc_x_zeta) - + serialize_fq_field_element(acc_y_zeta) - ).hex() - - result = { - "proof": { - "agg_at_zeta_proof": serialize_bls12_381_g1(phi_z_affine).hex(), - "column_commitments": column_commitments_concat, - "columns_at_zeta": columns_at_zeta_concat, - "lin_at_zeta_omega": serialize_fq_field_element(l_zeta_omega).hex(), - "lin_at_zeta_omega_proof": serialize_bls12_381_g1(phi_zw_affine).hex(), - "quotient_commitment": serialize_bls12_381_g1(c_q_affine).hex(), - }, - "verifier_key": { - "verification_key": verifier_key_bytes.hex(), - }, - "metadata": { - "parameters": { - "domain_size": params.domain_size, - "h": { - "x": serialize_fq_field_element(Blinding_Base[0]).hex(), - "y": serialize_fq_field_element(Blinding_Base[1]).hex(), - }, - "seed": { - "x": serialize_fq_field_element(SeedPoint[0]).hex(), - "y": serialize_fq_field_element(SeedPoint[1]).hex(), - }, - "result": { - "x": serialize_fq_field_element(result_point[0]).hex(), - "y": serialize_fq_field_element(result_point[1]).hex(), - }, - }, - }, - } - - output_file = output_dir / variant.name - output_file.parent.mkdir(parents=True, exist_ok=True) - with open(output_file, "w") as f: - json.dump(result, f, indent=2) - - print(f"✓ Proof exported to: {output_file}") - print(f" Proof size: {len(proof_bytes)} bytes") - print(f" Ring size: {params.max_ring_size} keys") - print(f" Domain size: {params.domain_size}") - - return result - - -def export_proof_to_json(output_dir: str | None = None, variants: list[VariantSpec] | None = None) -> list[dict[str, Any]]: - """ - Generate Python ring proofs and export them to JSON. - - Args: - output_dir: Directory to save JSON files (default: tests/vectors/others) - variants: Optional list of VariantSpec entries to export - - Returns: - List of dictionaries containing proofs and parameters - """ - if output_dir is None: - output_dir = str(Path(__file__).parent.parent.parent / "tests" / "vectors" / "others") - - output_path = Path(output_dir) - variant_list = variants or DEFAULT_VARIANTS - - results = [] - for variant in variant_list: - results.append(export_variant(variant, output_path)) - - return results - - -if __name__ == "__main__": - output_dir = sys.argv[1] if len(sys.argv) > 1 else None - export_proof_to_json(output_dir=output_dir) diff --git a/dot_ring/vrf/codec.py b/dot_ring/vrf/codec.py new file mode 100644 index 0000000..b54d285 --- /dev/null +++ b/dot_ring/vrf/codec.py @@ -0,0 +1,77 @@ +"""Bandersnatch VRF spec section 1.5 codec functions.""" + +from __future__ import annotations + +from functools import lru_cache + +from dot_ring.curve.curve import CurveVariant +from dot_ring.curve.point import CurvePoint +from dot_ring.curve.twisted_edwards.te_affine_point import TEAffinePoint + + +def scalar_len(curve: CurveVariant) -> int: + return (curve.curve.params.subgroup_order.bit_length() + 7) // 8 + + +def point_len(curve: CurveVariant) -> int: + encoding = curve.curve.params.encoding + return encoding.point_len * (2 if encoding.uncompressed else 1) + + +def enc_scalar(curve: CurveVariant, value: int) -> bytes: + return int(value % curve.curve.params.subgroup_order).to_bytes(scalar_len(curve), "little") + + +def dec_scalar(curve: CurveVariant, value: bytes) -> int: + if len(value) != scalar_len(curve): + raise ValueError(f"scalar must be exactly {scalar_len(curve)} bytes") + scalar = int.from_bytes(value, "little") + if scalar >= curve.curve.params.subgroup_order: + raise ValueError("scalar is not canonical") + return scalar + + +def dec_scalar_mod(curve: CurveVariant, value: bytes) -> int: + return int.from_bytes(value, "little") % curve.curve.params.subgroup_order + + +def enc_point(point: CurvePoint) -> bytes: + return point.point_to_string() + + +def dec_point(curve: CurveVariant, value: bytes) -> CurvePoint: + if len(value) != point_len(curve): + raise ValueError(f"point must be exactly {point_len(curve)} bytes") + point = curve.point_type.string_to_point(value) + if not valid_point(point): + raise ValueError("point is not a valid nonidentity subgroup point") + return point + + +def enc_64(value: int) -> bytes: + if not 0 <= value < 1 << 64: + raise ValueError("value does not fit in uint64") + return value.to_bytes(8, "little") + + +@lru_cache(maxsize=32) +def _cofactor_inverse(cofactor: int, subgroup_order: int) -> int: + return pow(cofactor, -1, subgroup_order) + + +def valid_point(point: CurvePoint) -> bool: + if point.is_identity(): + return False + + cofactor = point.curve.params.cofactor + if cofactor == 1: + return True + + cofactor_inv = _cofactor_inverse(cofactor, point.curve.params.subgroup_order) + if isinstance(point, TEAffinePoint): + cleared = TEAffinePoint.__mul__(point, cofactor) + else: + cleared = point * cofactor + if cleared.is_identity(): + return False + return cleared * cofactor_inv == point diff --git a/dot_ring/vrf/ietf/__init__.py b/dot_ring/vrf/ietf/__init__.py index 3369ced..c54196a 100644 --- a/dot_ring/vrf/ietf/__init__.py +++ b/dot_ring/vrf/ietf/__init__.py @@ -1,4 +1,4 @@ -from .thin import ThinBatchVerifier, ThinVRF +from .thin import ThinVRF from .tiny import TinyVRF -__all__ = ["ThinBatchVerifier", "ThinVRF", "TinyVRF"] +__all__ = ["ThinVRF", "TinyVRF"] diff --git a/dot_ring/vrf/ietf/thin.py b/dot_ring/vrf/ietf/thin.py index 9597e5b..516d16f 100644 --- a/dot_ring/vrf/ietf/thin.py +++ b/dot_ring/vrf/ietf/thin.py @@ -5,25 +5,20 @@ from __future__ import annotations +from collections.abc import Sequence from dataclasses import dataclass from typing import Any -from dot_ring.curve.curve import CurveVariant from dot_ring.curve.point import CurvePoint -from dot_ring.vrf.transcript import ( +from dot_ring.vrf.codec import dec_scalar, dec_scalar_mod, enc_point, enc_scalar, point_len, scalar_len, valid_point +from dot_ring.vrf.domain import DomSep +from dot_ring.vrf.primitives import ( CHALLENGE_LEN, - DomSep, VrfIo, challenge, nonce, - point_len, point_to_hash, - scalar_decode, - scalar_encode, - scalar_len, - schnorr_ios, squeeze_transcript_bytes, - suite_id, vrf_transcript, vrf_transcript_scalars, ) @@ -32,7 +27,7 @@ @dataclass -class ThinBatchItem: +class _ThinBatchItem: c: int ios: list[VrfIo] zs: list[int] @@ -56,19 +51,17 @@ def decode(cls, proof_bytes: bytes) -> ThinVRF: if len(proof_bytes) != expected: raise ValueError(f"invalid Thin VRF proof length: expected {expected}, got {len(proof_bytes)}") try: - output_point = cls.cv.string_to_point(proof_bytes[:encoded_point_len]) - r = cls.cv.string_to_point(proof_bytes[encoded_point_len : 2 * encoded_point_len]) + output_point = cls.cv.point_type.string_to_point(proof_bytes[:encoded_point_len]) + r = cls.cv.point_type.string_to_point(proof_bytes[encoded_point_len : 2 * encoded_point_len]) except ValueError as exc: raise ValueError("Invalid point in proof") from exc - s = scalar_decode(cls.cv, proof_bytes[2 * encoded_point_len :]) - if s >= cls.cv.curve.params.subgroup_order: - raise ValueError("Response scalar s is not less than the curve order") - if not (cls._valid_point(output_point) and cls._valid_point(r)): + s = dec_scalar(cls.cv, proof_bytes[2 * encoded_point_len :]) + if not (valid_point(output_point) and valid_point(r)): raise ValueError("Invalid identity or subgroup point in proof") return cls(output_point, r, s) def encode(self) -> bytes: - return self.output_point.point_to_string() + self.r.point_to_string() + scalar_encode(self.cv, self.s) + return enc_point(self.output_point) + enc_point(self.r) + enc_scalar(self.cv, self.s) @classmethod def prove( @@ -78,47 +71,38 @@ def prove( additional_data: bytes, salt: bytes = b"", ) -> ThinVRF: - secret_scalar = scalar_decode(cls.cv, secret_key) - public_key = cls.cv.generator_point() * secret_scalar - input_point = cls.cv.encode_to_curve(alpha, salt) + secret_scalar = dec_scalar_mod(cls.cv, secret_key) + public_key = cls.cv.point_type.generator_point() * secret_scalar + input_point = cls.cv.point_type.encode_to_curve(alpha, salt) output_point = input_point * secret_scalar - return cls.prove_ios([VrfIo(input_point, output_point)], secret_scalar, public_key, additional_data) - - @classmethod - def prove_ios( - cls, - ios: list[VrfIo], - secret_scalar: int, - public_key: CurvePoint, - additional_data: bytes, - ) -> ThinVRF: - """Spec section 3.1 steps 2-7 over caller-supplied I/O pairs.""" - transcript, merged = vrf_transcript(cls.cv, DomSep.THIN_VRF, schnorr_ios(cls.cv, public_key, ios), additional_data) + transcript, merged = vrf_transcript( + cls.cv, + DomSep.THIN_VRF, + [VrfIo(cls.cv.point_type.generator_point(), public_key), VrfIo(input_point, output_point)], + additional_data, + ) k = nonce(cls.cv, secret_scalar, transcript) r = merged.input * k c = challenge(cls.cv, [r], transcript) s = (k + c * secret_scalar) % cls.cv.curve.params.subgroup_order - output_point = ios[0].output if len(ios) == 1 else merged.output return cls(output_point, r, s) def verify(self, public_key: bytes, input: bytes, additional_data: bytes, salt: bytes = b"") -> bool: - input_point = self.cv.encode_to_curve(input, salt) + input_point = self.cv.point_type.encode_to_curve(input, salt) try: - public_key_pt = self.cv.string_to_point(public_key) + public_key_point = self.cv.point_type.string_to_point(public_key) except ValueError as exc: raise ValueError("Invalid public key") from exc - return self.verify_ios(public_key_pt, [VrfIo(input_point, self.output_point)], additional_data) - - def verify_ios(self, public_key: CurvePoint, ios: list[VrfIo], additional_data: bytes) -> bool: - """Spec section 3.2: validate inputs, reconstruct `T`, derive `c`, check `s*I_m = R + c*O_m`.""" - proof_points_valid = self._valid_point(public_key) and self._valid_point(self.r) - ios_valid = all(self._valid_point(io.input) and self._valid_point(io.output) for io in ios) - if not proof_points_valid or not ios_valid: + if not (valid_point(public_key_point) and valid_point(self.r) and valid_point(input_point) and valid_point(self.output_point)): return False - transcript, merged = vrf_transcript(self.cv, DomSep.THIN_VRF, schnorr_ios(self.cv, public_key, ios), additional_data) + transcript, merged = vrf_transcript( + self.cv, + DomSep.THIN_VRF, + [VrfIo(self.cv.point_type.generator_point(), public_key_point), VrfIo(input_point, self.output_point)], + additional_data, + ) c = challenge(self.cv, [self.r], transcript) - lhs = self.cv.msm([merged.input, merged.output], [self.s, -c]) - return lhs == self.r + return self.cv.point_type.msm([merged.input, merged.output], [self.s, -c]) == self.r @classmethod def proof_to_hash(cls, gamma: CurvePoint, mul_cofactor: bool = False) -> bytes: @@ -126,69 +110,51 @@ def proof_to_hash(cls, gamma: CurvePoint, mul_cofactor: bool = False) -> bytes: gamma = gamma * cls.cv.curve.params.cofactor return point_to_hash(cls.cv, gamma) - -def _batch_coefficients(items: list[ThinBatchItem], curve: CurveVariant) -> list[int]: - """Spec section 3.3.2: `suite_id || BatchVerify || enc_scalar(c_j) || enc_scalar(s_j)`.""" - if not items: - return [] - - absorbed = bytearray(suite_id(curve)) - absorbed.append(DomSep.BATCH_VERIFY) - for item in items: - absorbed.extend(scalar_encode(curve, item.c)) - absorbed.extend(scalar_encode(curve, item.s)) - - order = curve.curve.params.subgroup_order - raw = squeeze_transcript_bytes(curve.curve.params.hash_fn, bytes(absorbed), CHALLENGE_LEN * len(items)) - return [int.from_bytes(raw[CHALLENGE_LEN * index : CHALLENGE_LEN * (index + 1)], "little") % order for index in range(len(items))] - - -class ThinBatchVerifier: - """Accumulates spec section 3.3 items and verifies the weighted Thin equation.""" - - def __init__(self, curve: CurveVariant): - self.cv = curve - self.items: list[ThinBatchItem] = [] - self._invalid = False - @classmethod - def __class_getitem__(cls, curve_variant: CurveVariant | Any) -> type[ThinBatchVerifier] | Any: - if not isinstance(curve_variant, CurveVariant): - return cls - - class _SpecializedThinBatchVerifier(cls): - def __init__(self) -> None: - super().__init__(curve_variant) - - _SpecializedThinBatchVerifier.__name__ = f"{cls.__name__}[{curve_variant.name}]" - return _SpecializedThinBatchVerifier - - def push(self, public_key: CurvePoint, ios: list[VrfIo], additional_data: bytes, proof: ThinVRF) -> None: - proof_points_valid = VRF._valid_point(public_key) and VRF._valid_point(proof.r) - ios_valid = all(VRF._valid_point(io.input) and VRF._valid_point(io.output) for io in ios) - if not proof_points_valid or not ios_valid: - self._invalid = True - return - chained_ios = schnorr_ios(self.cv, public_key, ios) - transcript, scalar_stream = vrf_transcript_scalars(self.cv, DomSep.THIN_VRF, chained_ios, additional_data) - c = challenge(self.cv, [proof.r], transcript) - self.items.append(ThinBatchItem(c, chained_ios, scalar_stream, proof.r, proof.s)) - - def verify(self) -> bool: - if self._invalid: + def batch_verify( + cls, + proofs: Sequence[ThinVRF], + public_keys: Sequence[bytes], + inputs: Sequence[bytes], + additional_data: Sequence[bytes], + salts: Sequence[bytes] | None = None, + ) -> bool: + if salts is None: + salts = [b""] * len(proofs) + + items: list[_ThinBatchItem] = [] + try: + for proof, public_key, input_value, ad, salt in zip(proofs, public_keys, inputs, additional_data, salts, strict=True): + input_point = cls.cv.point_type.encode_to_curve(input_value, salt) + public_key_point = cls.cv.point_type.string_to_point(public_key) + if not (valid_point(public_key_point) and valid_point(proof.r) and valid_point(input_point) and valid_point(proof.output_point)): + return False + ios = [VrfIo(cls.cv.point_type.generator_point(), public_key_point), VrfIo(input_point, proof.output_point)] + transcript, scalars = vrf_transcript_scalars(cls.cv, DomSep.THIN_VRF, ios, ad) + items.append(_ThinBatchItem(challenge(cls.cv, [proof.r], transcript), ios, scalars, proof.r, proof.s)) + except (AttributeError, TypeError, ValueError): return False - if not self.items: + + if not items: return True + absorbed = bytearray(cls.cv.curve.params.suite_id) + absorbed.append(DomSep.BATCH_VERIFY) + for item in items: + absorbed.extend(enc_scalar(cls.cv, item.c)) + absorbed.extend(enc_scalar(cls.cv, item.s)) + + raw = squeeze_transcript_bytes(cls.cv.curve.params.hash_fn, bytes(absorbed), CHALLENGE_LEN * len(items)) points: list[CurvePoint] = [] - scalars: list[int] = [] - for coefficient, item in zip(_batch_coefficients(self.items, self.cv), self.items, strict=True): + msm_scalars: list[int] = [] + for index, item in enumerate(items): + coefficient = dec_scalar_mod(cls.cv, raw[CHALLENGE_LEN * index : CHALLENGE_LEN * (index + 1)]) weighted_c = coefficient * item.c weighted_s = coefficient * item.s for io, z in zip(item.ios, item.zs, strict=True): points.extend([io.input, io.output]) - scalars.extend([weighted_s * z, -(weighted_c * z)]) + msm_scalars.extend([weighted_s * z, -(weighted_c * z)]) points.append(item.r) - scalars.append(-coefficient) - return self.cv.msm(points, scalars).is_identity() + msm_scalars.append(-coefficient) + return cls.cv.point_type.msm(points, msm_scalars).is_identity() diff --git a/dot_ring/vrf/ietf/tiny.py b/dot_ring/vrf/ietf/tiny.py index 761149c..faefb21 100644 --- a/dot_ring/vrf/ietf/tiny.py +++ b/dot_ring/vrf/ietf/tiny.py @@ -9,20 +9,15 @@ from typing import Any from dot_ring.curve.point import CurvePoint -from dot_ring.vrf.transcript import ( +from dot_ring.vrf.codec import dec_scalar, dec_scalar_mod, enc_point, enc_scalar, point_len, scalar_len, valid_point +from dot_ring.vrf.domain import DomSep +from dot_ring.vrf.primitives import ( CHALLENGE_LEN, - DomSep, VrfIo, challenge, nonce, - point_len, point_to_hash, - scalar_decode, - scalar_encode, - scalar_len, - schnorr_ios, vrf_transcript, - vrf_transcript_scalars, ) from ..vrf import VRF @@ -44,26 +39,17 @@ def decode(cls, proof_bytes: bytes) -> TinyVRF: if len(proof_bytes) != expected: raise ValueError(f"invalid Tiny VRF proof length: expected {expected}, got {len(proof_bytes)}") try: - output_point = cls.cv.string_to_point(proof_bytes[:encoded_point_len]) + output_point = cls.cv.point_type.string_to_point(proof_bytes[:encoded_point_len]) except ValueError as exc: raise ValueError("Invalid output point") from exc - order = cls.cv.curve.params.subgroup_order - c = int.from_bytes(proof_bytes[encoded_point_len : encoded_point_len + CHALLENGE_LEN], "little") % order - s = scalar_decode(cls.cv, proof_bytes[encoded_point_len + CHALLENGE_LEN :]) - if s >= order: - raise ValueError("Response scalar s is not less than the curve order") - if not cls._valid_point(output_point): + c = dec_scalar_mod(cls.cv, proof_bytes[encoded_point_len : encoded_point_len + CHALLENGE_LEN]) + s = dec_scalar(cls.cv, proof_bytes[encoded_point_len + CHALLENGE_LEN :]) + if not valid_point(output_point): raise ValueError("Invalid identity or subgroup point in proof") return cls(output_point, c, s) def encode(self) -> bytes: - return self.output_point.point_to_string() + self.c.to_bytes(CHALLENGE_LEN, "little") + scalar_encode(self.cv, self.s) - - @classmethod - def _io_from_alpha(cls, alpha: bytes, secret_scalar: int, salt: bytes = b"") -> VrfIo: - input_point = cls.cv.encode_to_curve(alpha, salt) - output_point = input_point * secret_scalar - return VrfIo(input_point, output_point) + return enc_point(self.output_point) + self.c.to_bytes(CHALLENGE_LEN, "little") + enc_scalar(self.cv, self.s) @classmethod def prove( @@ -73,85 +59,30 @@ def prove( additional_data: bytes, salt: bytes = b"", ) -> TinyVRF: - secret_scalar = scalar_decode(cls.cv, secret_key) - public_key = cls.cv.generator_point() * secret_scalar - io = cls._io_from_alpha(alpha, secret_scalar, salt) - return cls.prove_ios([io], secret_scalar, public_key, additional_data) - - @classmethod - def prove_ios( - cls, - ios: list[VrfIo], - secret_scalar: int, - public_key: CurvePoint, - additional_data: bytes, - ) -> TinyVRF: - """Spec section 2.1 steps 2-7 over caller-supplied I/O pairs.""" - if len(ios) == 1: - transcript, scalars = vrf_transcript_scalars( - cls.cv, - DomSep.TINY_VRF, - schnorr_ios(cls.cv, public_key, ios), - additional_data, - ) - z0, z1 = scalars - k = nonce(cls.cv, secret_scalar, transcript) - generator = cls.cv.generator_point() - order = cls.cv.curve.params.subgroup_order - r = cls.cv.msm( - [generator, ios[0].input], - [(k * z0) % order, (k * z1) % order], - ) - c = challenge(cls.cv, [r], transcript) - s = (k + c * secret_scalar) % order - return cls(ios[0].output, c, s) - - transcript, merged = vrf_transcript(cls.cv, DomSep.TINY_VRF, schnorr_ios(cls.cv, public_key, ios), additional_data) + secret_scalar = dec_scalar_mod(cls.cv, secret_key) + public_key = cls.cv.point_type.generator_point() * secret_scalar + input_point = cls.cv.point_type.encode_to_curve(alpha, salt) + output_point = input_point * secret_scalar + ios = [VrfIo(cls.cv.point_type.generator_point(), public_key), VrfIo(input_point, output_point)] + transcript, merged = vrf_transcript(cls.cv, DomSep.TINY_VRF, ios, additional_data) k = nonce(cls.cv, secret_scalar, transcript) r = merged.input * k c = challenge(cls.cv, [r], transcript) s = (k + c * secret_scalar) % cls.cv.curve.params.subgroup_order - output_point = ios[0].output if len(ios) == 1 else merged.output return cls(output_point, c, s) def verify(self, public_key: bytes, input: bytes, additional_data: bytes, salt: bytes = b"") -> bool: - input_point = self.cv.encode_to_curve(input, salt) + input_point = self.cv.point_type.encode_to_curve(input, salt) try: - public_key_pt = self.cv.string_to_point(public_key) + public_key_pt = self.cv.point_type.string_to_point(public_key) except ValueError as exc: raise ValueError("Invalid public key") from exc - io = VrfIo(input_point, self.output_point) - return self.verify_ios(public_key_pt, [io], additional_data) - def verify_ios(self, public_key: CurvePoint, ios: list[VrfIo], additional_data: bytes) -> bool: - """Spec section 2.2: validate inputs, reconstruct `R = s*I_m - c*O_m`, then compare challenges.""" - ios_valid = all(self._valid_point(io.input) and self._valid_point(io.output) for io in ios) - if not self._valid_point(public_key) or not ios_valid: + if not valid_point(public_key_pt) or not valid_point(input_point) or not valid_point(self.output_point): return False - if len(ios) == 1: - transcript, scalars = vrf_transcript_scalars( - self.cv, - DomSep.TINY_VRF, - schnorr_ios(self.cv, public_key, ios), - additional_data, - ) - z0, z1 = scalars - generator = self.cv.generator_point() - order = self.cv.curve.params.subgroup_order - r = self.cv.msm( - [generator, ios[0].input, public_key, ios[0].output], - [ - (self.s * z0) % order, - (self.s * z1) % order, - (-self.c * z0) % order, - (-self.c * z1) % order, - ], - ) - expected_c = challenge(self.cv, [r], transcript) - return self.c == expected_c - - transcript, merged = vrf_transcript(self.cv, DomSep.TINY_VRF, schnorr_ios(self.cv, public_key, ios), additional_data) - r = self.cv.msm([merged.input, merged.output], [self.s, -self.c]) + ios = [VrfIo(self.cv.point_type.generator_point(), public_key_pt), VrfIo(input_point, self.output_point)] + transcript, merged = vrf_transcript(self.cv, DomSep.TINY_VRF, ios, additional_data) + r = self.cv.point_type.msm([merged.input, merged.output], [self.s, -self.c]) expected_c = challenge(self.cv, [r], transcript) return self.c == expected_c @@ -173,16 +104,13 @@ def ecvrf_decode_proof(cls, proof: bytes | str) -> tuple[CurvePoint, int, int]: raise ValueError(f"invalid Tiny VRF proof length: expected {expected}, got {len(proof)}") try: - output_point = cls.cv.string_to_point(proof[:encoded_point_len]) + output_point = cls.cv.point_type.string_to_point(proof[:encoded_point_len]) except ValueError as exc: raise ValueError("Invalid gamma point") from exc - order = cls.cv.curve.params.subgroup_order - c = int.from_bytes(proof[encoded_point_len : encoded_point_len + CHALLENGE_LEN], "little") % order - s = scalar_decode(cls.cv, proof[encoded_point_len + CHALLENGE_LEN :]) - if s >= order: - raise ValueError("Response scalar S is not less than the curve order") - if not cls._valid_point(output_point): + c = dec_scalar_mod(cls.cv, proof[encoded_point_len : encoded_point_len + CHALLENGE_LEN]) + s = dec_scalar(cls.cv, proof[encoded_point_len + CHALLENGE_LEN :]) + if not valid_point(output_point): raise ValueError("Invalid identity or subgroup point in proof") return output_point, c, s diff --git a/dot_ring/vrf/io.py b/dot_ring/vrf/io.py deleted file mode 100644 index f8b6fad..0000000 --- a/dot_ring/vrf/io.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - -from dot_ring.curve.point import CurvePoint - - -@dataclass(frozen=True) -class VrfIo: - input: CurvePoint - output: CurvePoint - - def encode(self) -> bytes: - return self.input.point_to_string() + self.output.point_to_string() diff --git a/dot_ring/vrf/pedersen/__init__.py b/dot_ring/vrf/pedersen/__init__.py index 9d25e5e..3f270c7 100644 --- a/dot_ring/vrf/pedersen/__init__.py +++ b/dot_ring/vrf/pedersen/__init__.py @@ -1,4 +1,3 @@ -from .batch_verifier import PedersenBatchVerifier from .vrf import PedersenVRF -__all__ = ["PedersenBatchVerifier", "PedersenVRF"] +__all__ = ["PedersenVRF"] diff --git a/dot_ring/vrf/pedersen/batch_verifier.py b/dot_ring/vrf/pedersen/batch_verifier.py deleted file mode 100644 index fe27c78..0000000 --- a/dot_ring/vrf/pedersen/batch_verifier.py +++ /dev/null @@ -1,332 +0,0 @@ -"""Pedersen VRF batch verification from Bandersnatch VRF spec section 4.4.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any - -from dot_ring.curve.curve import CurveVariant -from dot_ring.curve.point import CurvePoint -from dot_ring.vrf.transcript import CHALLENGE_LEN, DomSep, VrfIo, challenge, squeeze_transcript_bytes, suite_id, vrf_transcript_scalars - -from .vrf import PedersenVRF, _blinding_base - - -@dataclass -class _PedersenBatchItem: - c: int - ios: list[VrfIo] - zs: list[int] - pk_com: CurvePoint - r: CurvePoint - ok: CurvePoint - s: int - sb: int - - -def _signed_mod(value: int, order: int) -> int: - value %= order - if value > order >> 1: - value -= order - return value - - -def _fold_duplicate_points_by_identity(points: list[CurvePoint], scalars: list[int], order: int) -> tuple[list[CurvePoint], list[int]]: - seen: set[int] = set() - has_duplicate = False - for point in points: - point_id = id(point) - if point_id in seen: - has_duplicate = True - break - seen.add(point_id) - if not has_duplicate: - return points, scalars - - points_by_id: dict[int, CurvePoint] = {} - scalars_by_id: dict[int, int] = {} - for point, scalar in zip(points, scalars, strict=True): - scalar %= order - if scalar == 0: - continue - point_id = id(point) - if point_id in scalars_by_id: - scalars_by_id[point_id] = (scalars_by_id[point_id] + scalar) % order - else: - points_by_id[point_id] = point - scalars_by_id[point_id] = scalar - - folded_points = [] - folded_scalars = [] - for point_id, point in points_by_id.items(): - scalar = _signed_mod(scalars_by_id[point_id], order) - if scalar: - folded_points.append(point) - folded_scalars.append(scalar) - return folded_points, folded_scalars - - -def _batch_coefficient_pairs_from_bytes(item_count: int, item_bytes: bytes | bytearray, curve: CurveVariant) -> list[tuple[int, int]]: - """Spec section 4.4.2: transcript weights from scalar triples only, in item order.""" - if item_count == 0: - return [] - - absorbed = bytearray(suite_id(curve)) - absorbed.append(DomSep.BATCH_VERIFY) - absorbed.extend(item_bytes) - - order = curve.curve.params.subgroup_order - raw = squeeze_transcript_bytes(curve.curve.params.hash_fn, bytes(absorbed), 2 * CHALLENGE_LEN * item_count) - pairs = [] - for index in range(item_count): - offset = 2 * CHALLENGE_LEN * index - io_weight = int.from_bytes(raw[offset : offset + CHALLENGE_LEN], "little") % order - commitment_weight = int.from_bytes(raw[offset + CHALLENGE_LEN : offset + 2 * CHALLENGE_LEN], "little") % order - pairs.append((io_weight, commitment_weight)) - return pairs - - -def _batch_coefficient_item_bytes(item: _PedersenBatchItem, scalar_size: int, order: int) -> bytes: - return ( - int(item.c % order).to_bytes(scalar_size, "little") - + int(item.s % order).to_bytes(scalar_size, "little") - + int(item.sb % order).to_bytes(scalar_size, "little") - ) - - -def _batch_coefficient_pairs(items: list[_PedersenBatchItem], curve: CurveVariant) -> list[tuple[int, int]]: - order = curve.curve.params.subgroup_order - scalar_size = (order.bit_length() + 7) // 8 - item_bytes = bytearray() - for item in items: - item_bytes.extend(_batch_coefficient_item_bytes(item, scalar_size, order)) - return _batch_coefficient_pairs_from_bytes(len(items), item_bytes, curve) - - -class PedersenBatchVerifier: - """Accumulates spec section 4.4 items and verifies the two weighted equations together.""" - - def __init__(self, curve: CurveVariant): - self.cv = curve - self.items: list[_PedersenBatchItem] = [] - self._order = curve.curve.params.subgroup_order - self._scalar_size = (self._order.bit_length() + 7) // 8 - self._coefficient_item_bytes = bytearray() - self._single_io_items = True - self._invalid = False - - @classmethod - def __class_getitem__(cls, curve_variant: CurveVariant | Any) -> type[PedersenBatchVerifier] | Any: - if not isinstance(curve_variant, CurveVariant): - return cls - - class _SpecializedPedersenBatchVerifier(cls): - def __init__(self) -> None: - super().__init__(curve_variant) - - _SpecializedPedersenBatchVerifier.__name__ = f"{cls.__name__}[{curve_variant.name}]" - return _SpecializedPedersenBatchVerifier - - def _item( - self, - ios: list[VrfIo], - additional_data: bytes, - proof: PedersenVRF, - ) -> _PedersenBatchItem: - if not PedersenVRF._valid_ios(ios): - return self._invalid_item(proof) - return self._item_from_trusted_ios(ios, additional_data, proof) - - def _item_from_trusted_ios( - self, - ios: list[VrfIo], - additional_data: bytes, - proof: PedersenVRF, - ) -> _PedersenBatchItem: - if not proof._valid_proof_points(): - return self._invalid_item(proof) - if len(ios) == 1: - return self._single_io_item(ios[0], additional_data, proof) - - transcript, zs = vrf_transcript_scalars(self.cv, DomSep.PEDERSEN_VRF, ios, additional_data) - transcript.absorb(proof.blinded_pk.point_to_string()) - c = challenge(self.cv, [proof.result_point, proof.ok], transcript) - return _PedersenBatchItem( - c=c, - ios=list(ios), - zs=zs, - pk_com=proof.blinded_pk, - r=proof.result_point, - ok=proof.ok, - s=proof.s, - sb=proof.sb, - ) - - def _invalid_item(self, proof: PedersenVRF) -> _PedersenBatchItem: - self._invalid = True - return _PedersenBatchItem(0, [], [], proof.blinded_pk, proof.result_point, proof.ok, proof.s, proof.sb) - - def _single_io_item(self, io: VrfIo, additional_data: bytes, proof: PedersenVRF) -> _PedersenBatchItem: - absorbed = bytearray(suite_id(self.cv)) - absorbed.append(DomSep.PEDERSEN_VRF) - absorbed.extend((1).to_bytes(8, "little")) - absorbed.extend(io.input.point_to_string()) - absorbed.extend(io.output.point_to_string()) - absorbed.extend(len(additional_data).to_bytes(8, "little")) - absorbed.extend(additional_data) - absorbed.extend(proof.blinded_pk.point_to_string()) - absorbed.append(DomSep.CHALLENGE) - absorbed.extend(proof.result_point.point_to_string()) - absorbed.extend(proof.ok.point_to_string()) - c = _challenge_scalar_from_absorbed(self.cv, bytes(absorbed)) - return _PedersenBatchItem( - c=c, - ios=[io], - zs=[1], - pk_com=proof.blinded_pk, - r=proof.result_point, - ok=proof.ok, - s=proof.s, - sb=proof.sb, - ) - - def _push_item(self, item: _PedersenBatchItem) -> None: - if self._invalid: - return - self.items.append(item) - self._single_io_items = self._single_io_items and len(item.ios) == 1 and len(item.zs) == 1 and item.zs[0] == 1 - self._coefficient_item_bytes.extend(_batch_coefficient_item_bytes(item, self._scalar_size, self._order)) - - def push(self, ios: list[VrfIo], additional_data: bytes, proof: PedersenVRF) -> None: - self._push_item(self._item(ios, additional_data, proof)) - - def verify(self) -> bool: - if self._invalid: - return False - if not self.items: - return True - - blinding_base = _blinding_base(self.cv) - generator = self.cv.generator_point() - order = self._order - expected_bytes = len(self.items) * 3 * self._scalar_size - if len(self._coefficient_item_bytes) == expected_bytes: - coefficients = _batch_coefficient_pairs_from_bytes(len(self.items), self._coefficient_item_bytes, self.cv) - else: - coefficients = _batch_coefficient_pairs(self.items, self.cv) - - if self._single_io_items: - points, scalars = self._verification_terms_single_io(coefficients, generator, blinding_base) - points, scalars = _fold_duplicate_points_by_identity(points, scalars, order) - return self.cv.msm(points, scalars).is_identity() - - points: list[CurvePoint] = [] - scalars: list[int] = [] - generator_scalar = 0 - blinding_scalar = 0 - - for item, (io_weight, commitment_weight) in zip(self.items, coefficients, strict=True): - for io, z in zip(item.ios, item.zs, strict=True): - points.extend([io.input, io.output]) - scalars.extend( - [ - _signed_mod(io_weight * item.s * z, order), - _signed_mod(-(io_weight * item.c * z), order), - ] - ) - points.append(item.ok) - scalars.append(-io_weight) - - generator_scalar = (generator_scalar + commitment_weight * item.s) % order - blinding_scalar = (blinding_scalar + commitment_weight * item.sb) % order - - points.extend([item.pk_com, item.r]) - scalars.extend( - [ - _signed_mod(-(commitment_weight * item.c), order), - -commitment_weight, - ] - ) - - if generator_scalar: - points.append(generator) - scalars.append(_signed_mod(generator_scalar, order)) - if blinding_scalar: - points.append(blinding_base) - scalars.append(_signed_mod(blinding_scalar, order)) - - points, scalars = _fold_duplicate_points_by_identity(points, scalars, order) - return self.cv.msm(points, scalars).is_identity() - - def _verification_terms_single_io( - self, - coefficients: list[tuple[int, int]], - generator: CurvePoint, - blinding_base: CurvePoint, - ) -> tuple[list[CurvePoint], list[int]]: - items = self.items - order = self._order - half_order = order >> 1 - point_count = len(items) * 5 + 2 - points: list[CurvePoint | None] = [None] * point_count - scalars = [0] * point_count - generator_scalar = 0 - blinding_scalar = 0 - pos = 0 - - for item, (io_weight, commitment_weight) in zip(items, coefficients, strict=True): - io = item.ios[0] - - value = (io_weight * item.s) % order - if value > half_order: - value -= order - points[pos] = io.input - scalars[pos] = value - pos += 1 - - value = -(io_weight * item.c) % order - if value > half_order: - value -= order - points[pos] = io.output - scalars[pos] = value - pos += 1 - - points[pos] = item.ok - scalars[pos] = -io_weight - pos += 1 - - generator_scalar = (generator_scalar + commitment_weight * item.s) % order - blinding_scalar = (blinding_scalar + commitment_weight * item.sb) % order - - value = -(commitment_weight * item.c) % order - if value > half_order: - value -= order - points[pos] = item.pk_com - scalars[pos] = value - pos += 1 - - points[pos] = item.r - scalars[pos] = -commitment_weight - pos += 1 - - if generator_scalar: - value = generator_scalar - if value > half_order: - value -= order - points[pos] = generator - scalars[pos] = value - pos += 1 - if blinding_scalar: - value = blinding_scalar - if value > half_order: - value -= order - points[pos] = blinding_base - scalars[pos] = value - pos += 1 - - return points[:pos], scalars[:pos] # type: ignore[return-value] - - -def _challenge_scalar_from_absorbed(curve: CurveVariant, absorbed: bytes) -> int: - rnd = squeeze_transcript_bytes(curve.curve.params.hash_fn, absorbed, CHALLENGE_LEN) - return int.from_bytes(rnd, "little") % curve.curve.params.subgroup_order diff --git a/dot_ring/vrf/pedersen/vrf.py b/dot_ring/vrf/pedersen/vrf.py index 99d73d5..f1c52b8 100644 --- a/dot_ring/vrf/pedersen/vrf.py +++ b/dot_ring/vrf/pedersen/vrf.py @@ -6,36 +6,28 @@ from __future__ import annotations +from collections.abc import Sequence from dataclasses import dataclass, field from typing import Any, TypeVar from dot_ring.curve.curve import CurveVariant from dot_ring.curve.point import CurvePoint -from dot_ring.vrf.transcript import ( - DomSep, +from dot_ring.vrf.codec import dec_scalar, dec_scalar_mod, enc_64, enc_point, enc_scalar, point_len, scalar_len, valid_point +from dot_ring.vrf.domain import DomSep +from dot_ring.vrf.primitives import ( + CHALLENGE_LEN, VrfIo, challenge, nonce, - point_len, point_to_hash, - scalar_decode, - scalar_encode, - scalar_len, + squeeze_transcript_bytes, vrf_transcript, ) - -from ..vrf import VRF +from dot_ring.vrf.vrf import VRF C = TypeVar("C", bound=CurveVariant) -def _blinding_base(curve: CurveVariant) -> CurvePoint: - blinding_base = curve.curve.params.auxiliary_points.blinding_base - if blinding_base is None: - raise ValueError(f"{curve.name} does not define a Pedersen blinding base") - return curve.point(blinding_base[0], blinding_base[1]) - - @dataclass(frozen=True) class PedersenVRF(VRF[C]): """Pedersen VRF proof plus gamma envelope. `_blinding_factor` is prover-local for Ring VRF.""" @@ -63,20 +55,15 @@ def decode(cls, proof: bytes) -> PedersenVRF: raise ValueError(f"invalid Pedersen VRF proof length: expected {expected}, got {len(proof)}") try: - output_point = cls.cv.string_to_point(proof[0:point_length]) - public_key_cp = cls.cv.string_to_point(proof[point_length : 2 * point_length]) - r = cls.cv.string_to_point(proof[2 * point_length : 3 * point_length]) - ok = cls.cv.string_to_point(proof[3 * point_length : 4 * point_length]) + output_point = cls.cv.point_type.string_to_point(proof[0:point_length]) + public_key_cp = cls.cv.point_type.string_to_point(proof[point_length : 2 * point_length]) + r = cls.cv.point_type.string_to_point(proof[2 * point_length : 3 * point_length]) + ok = cls.cv.point_type.string_to_point(proof[3 * point_length : 4 * point_length]) except ValueError as exc: raise ValueError("Invalid point in proof") from exc - s = scalar_decode(cls.cv, proof[4 * point_length : 4 * point_length + scalar_size]) - sb = scalar_decode(cls.cv, proof[4 * point_length + scalar_size :]) - order = cls.cv.curve.params.subgroup_order - if s >= order: - raise ValueError("Response scalar s is not canonical") - if sb >= order: - raise ValueError("Response scalar sb is not canonical") - if not all(cls._valid_point(point) for point in (output_point, public_key_cp, r, ok)): + s = dec_scalar(cls.cv, proof[4 * point_length : 4 * point_length + scalar_size]) + sb = dec_scalar(cls.cv, proof[4 * point_length + scalar_size :]) + if not all(valid_point(point) for point in (output_point, public_key_cp, r, ok)): raise ValueError("Invalid identity or subgroup point in proof") return cls( @@ -91,20 +78,14 @@ def decode(cls, proof: bytes) -> PedersenVRF: def encode(self) -> bytes: return ( - self.output_point.point_to_string() - + self.blinded_pk.point_to_string() - + self.result_point.point_to_string() - + self.ok.point_to_string() - + scalar_encode(self.cv, self.s) - + scalar_encode(self.cv, self.sb) + enc_point(self.output_point) + + enc_point(self.blinded_pk) + + enc_point(self.result_point) + + enc_point(self.ok) + + enc_scalar(self.cv, self.s) + + enc_scalar(self.cv, self.sb) ) - @classmethod - def _io_from_alpha(cls, alpha: bytes, secret_scalar: int, salt: bytes = b"") -> VrfIo: - input_point = cls.cv.encode_to_curve(alpha, salt) - output_point = input_point * secret_scalar - return VrfIo(input_point, output_point) - @classmethod def prove( cls, @@ -113,49 +94,30 @@ def prove( additional_data: bytes, salt: bytes = b"", ) -> PedersenVRF: - secret_scalar = scalar_decode(cls.cv, secret_key) - public_key = cls.cv.generator_point() * secret_scalar - io = cls._io_from_alpha(alpha, secret_scalar, salt) - return cls._prove_ios([io], secret_scalar, public_key, additional_data, points_validated=True) - - @classmethod - def prove_ios( - cls, - ios: list[VrfIo], - secret_scalar: int, - public_key: CurvePoint, - additional_data: bytes, - ) -> PedersenVRF: - return cls._prove_ios(ios, secret_scalar, public_key, additional_data, points_validated=False) + secret_scalar = dec_scalar_mod(cls.cv, secret_key) + public_key = cls.cv.point_type.generator_point() * secret_scalar + input_point = cls.cv.point_type.encode_to_curve(alpha, salt) + output_point = input_point * secret_scalar + io = VrfIo(input_point, output_point) - @classmethod - def _prove_ios( - cls, - ios: list[VrfIo], - secret_scalar: int, - public_key: CurvePoint, - additional_data: bytes, - *, - points_validated: bool, - ) -> PedersenVRF: - """Spec section 4.1 steps 1-10 over caller-supplied I/O pairs.""" - transcript, merged = vrf_transcript(cls.cv, DomSep.PEDERSEN_VRF, ios, additional_data) + transcript, merged = vrf_transcript(cls.cv, DomSep.PEDERSEN_VRF, [io], additional_data) blinding_factor = cls.blinding_scalar(secret_scalar, transcript) - blinding_base = _blinding_base(cls.cv) - generator = cls.cv.generator_point() + if not cls.cv.curve.params.auxiliary_points.blinding_base: + raise ValueError("Curve does not have a blinding base point for Pedersen VRF") + blinding_base = cls.cv.point_type(*cls.cv.curve.params.auxiliary_points.blinding_base) + generator = cls.cv.point_type.generator_point() blinded_pk = public_key + blinding_base * blinding_factor - transcript.absorb(blinded_pk.point_to_string()) + transcript.absorb(enc_point(blinded_pk)) k = nonce(cls.cv, secret_scalar, transcript) kb = nonce(cls.cv, blinding_factor, transcript) - result_point = cls.cv.msm([generator, blinding_base], [k, kb]) + result_point = cls.cv.point_type.msm([generator, blinding_base], [k, kb]) ok = merged.input * k c = challenge(cls.cv, [result_point, ok], transcript) order = cls.cv.curve.params.subgroup_order s = (k + c * secret_scalar) % order sb = (kb + c * blinding_factor) % order - output_point = ios[0].output if len(ios) == 1 else merged.output return cls( output_point=output_point, @@ -165,25 +127,32 @@ def _prove_ios( s=s, sb=sb, _blinding_factor=blinding_factor, - _points_validated=points_validated, + _points_validated=True, ) def verify(self, input: bytes, additional_data: bytes, salt: bytes = b"") -> bool: if not self._valid_proof_points(): return False - input_point = self.cv.encode_to_curve(input, salt) - return self._verify_ios([VrfIo(input_point, self.output_point)], additional_data) + input_point = self.cv.point_type.encode_to_curve(input, salt) + transcript, merged = vrf_transcript(self.cv, DomSep.PEDERSEN_VRF, [VrfIo(input_point, self.output_point)], additional_data) + transcript.absorb(enc_point(self.blinded_pk)) + c = challenge(self.cv, [self.result_point, self.ok], transcript) + if not self.cv.curve.params.auxiliary_points.blinding_base: + raise ValueError("Curve does not have a blinding base point for Pedersen VRF") + blinding_base = self.cv.point_type(*self.cv.curve.params.auxiliary_points.blinding_base) + generator = self.cv.point_type.generator_point() - def verify_ios(self, ios: list[VrfIo], additional_data: bytes) -> bool: - """Spec section 4.2: validate points, rebuild `T`, then check VRF and commitment equations.""" - if not self._valid_proof_points() or not self._valid_ios(ios): + lhs1 = self.cv.point_type.msm([merged.input, merged.output], [self.s, -c]) + if lhs1 != self.ok: return False - return self._verify_ios(ios, additional_data) + + lhs2 = self.cv.point_type.msm([generator, blinding_base, self.blinded_pk], [self.s, self.sb, -c]) + return lhs2 == self.result_point def _valid_proof_points(self) -> bool: if self._points_validated: return True - return all(self._valid_point(point) for point in (self.output_point, self.blinded_pk, self.result_point, self.ok)) + return all(valid_point(point) for point in (self.output_point, self.blinded_pk, self.result_point, self.ok)) @classmethod def _valid_ios(cls, ios: list[VrfIo]) -> bool: @@ -194,24 +163,10 @@ def _valid_ios(cls, ios: list[VrfIo]) -> bool: if point_id in seen: continue seen.add(point_id) - if not cls._valid_point(point): + if not valid_point(point): return False return True - def _verify_ios(self, ios: list[VrfIo], additional_data: bytes) -> bool: - transcript, merged = vrf_transcript(self.cv, DomSep.PEDERSEN_VRF, ios, additional_data) - transcript.absorb(self.blinded_pk.point_to_string()) - c = challenge(self.cv, [self.result_point, self.ok], transcript) - blinding_base = _blinding_base(self.cv) - generator = self.cv.generator_point() - - lhs1 = self.cv.msm([merged.input, merged.output], [self.s, -c]) - if lhs1 != self.ok: - return False - - lhs2 = self.cv.msm([generator, blinding_base, self.blinded_pk], [self.s, self.sb, -c]) - return lhs2 == self.result_point - def verify_unblinding(self, public_key: bytes | str | CurvePoint, blinding_factor: int) -> bool: """Spec section 4.3: check `Y_bar = Y + b*B` for a revealed blinding factor.""" order = self.cv.curve.params.subgroup_order @@ -219,13 +174,17 @@ def verify_unblinding(self, public_key: bytes | str | CurvePoint, blinding_facto return False if isinstance(public_key, bytes | str): try: - public_key = self.cv.string_to_point(public_key) + public_key = self.cv.point_type.string_to_point(public_key) except ValueError as exc: raise ValueError("Invalid public key") from exc - blinded_pk_valid = self._points_validated or self._valid_point(self.blinded_pk) - if not (self._valid_point(public_key) and blinded_pk_valid): + blinded_pk_valid = self._points_validated or valid_point(self.blinded_pk) + if not (valid_point(public_key) and blinded_pk_valid): return False - return public_key + _blinding_base(self.cv) * blinding_factor == self.blinded_pk + if not self.cv.curve.params.auxiliary_points.blinding_base: + raise ValueError("Curve does not have a blinding base point for Pedersen VRF") + blinding_base = self.cv.point_type(*self.cv.curve.params.auxiliary_points.blinding_base) + + return public_key + blinding_base * blinding_factor == self.blinded_pk @classmethod def blinding_scalar(cls, secret_scalar: int, transcript: Any) -> int: @@ -244,7 +203,162 @@ def ecvrf_proof_to_hash(cls, output_point_bytes: bytes | str) -> bytes: if not isinstance(output_point_bytes, bytes): output_point_bytes = bytes.fromhex(output_point_bytes) try: - output_point = cls.cv.string_to_point(output_point_bytes) + output_point = cls.cv.point_type.string_to_point(output_point_bytes) except ValueError as exc: raise ValueError("Invalid output point") from exc return cls.proof_to_hash(output_point) + + @classmethod + def batch_verify( + cls, + proofs: Sequence[PedersenVRF], + inputs: Sequence[bytes], + additional_data: Sequence[bytes], + salts: Sequence[bytes] | None = None, + ) -> bool: + if salts is None: + salts = [b""] * len(proofs) + + input_points: list[CurvePoint] = [] + try: + for input_value, salt in zip(inputs, salts, strict=True): + input_points.append(cls.cv.point_type.encode_to_curve(input_value, salt)) + except (AttributeError, TypeError, ValueError): + return False + batch_items: list[tuple[PedersenVRF, CurvePoint, int]] = [] + order = cls.cv.curve.params.subgroup_order + half_order = order >> 1 + coefficient_item_bytes = bytearray() + + try: + for proof, input_point, ad in zip(proofs, input_points, additional_data, strict=True): + if proof.cv.name != cls.cv.name or not proof._valid_proof_points(): + return False + + absorbed = bytearray(cls.cv.curve.params.suite_id) + absorbed.append(DomSep.PEDERSEN_VRF) + absorbed.extend(enc_64(1)) + absorbed.extend(enc_point(input_point)) + absorbed.extend(enc_point(proof.output_point)) + absorbed.extend(enc_64(len(ad))) + absorbed.extend(ad) + absorbed.extend(enc_point(proof.blinded_pk)) + absorbed.append(DomSep.CHALLENGE) + absorbed.extend(enc_point(proof.result_point)) + absorbed.extend(enc_point(proof.ok)) + c = dec_scalar_mod(cls.cv, squeeze_transcript_bytes(cls.cv.curve.params.hash_fn, bytes(absorbed), CHALLENGE_LEN)) + + batch_items.append((proof, input_point, c)) + coefficient_item_bytes.extend(enc_scalar(cls.cv, c)) + coefficient_item_bytes.extend(enc_scalar(cls.cv, proof.s)) + coefficient_item_bytes.extend(enc_scalar(cls.cv, proof.sb)) + except (AttributeError, TypeError, ValueError): + return False + + if not batch_items: + return True + + absorbed = bytearray(cls.cv.curve.params.suite_id) + absorbed.append(DomSep.BATCH_VERIFY) + absorbed.extend(coefficient_item_bytes) + weights = squeeze_transcript_bytes(cls.cv.curve.params.hash_fn, bytes(absorbed), 2 * CHALLENGE_LEN * len(batch_items)) + + point_count = len(batch_items) * 5 + 2 + points: list[CurvePoint | None] = [None] * point_count + scalars = [0] * point_count + generator_scalar = 0 + blinding_scalar = 0 + pos = 0 + + for index, (proof, input_point, c) in enumerate(batch_items): + offset = 2 * CHALLENGE_LEN * index + io_weight = dec_scalar_mod(cls.cv, weights[offset : offset + CHALLENGE_LEN]) + commitment_weight = dec_scalar_mod(cls.cv, weights[offset + CHALLENGE_LEN : offset + 2 * CHALLENGE_LEN]) + + value = (io_weight * proof.s) % order + if value > half_order: + value -= order + points[pos] = input_point + scalars[pos] = value + pos += 1 + + value = -(io_weight * c) % order + if value > half_order: + value -= order + points[pos] = proof.output_point + scalars[pos] = value + pos += 1 + + points[pos] = proof.ok + scalars[pos] = -io_weight + pos += 1 + + generator_scalar = (generator_scalar + commitment_weight * proof.s) % order + blinding_scalar = (blinding_scalar + commitment_weight * proof.sb) % order + + value = -(commitment_weight * c) % order + if value > half_order: + value -= order + points[pos] = proof.blinded_pk + scalars[pos] = value + pos += 1 + + points[pos] = proof.result_point + scalars[pos] = -commitment_weight + pos += 1 + + if not cls.cv.curve.params.auxiliary_points.blinding_base: + raise ValueError("Curve does not have a blinding base point for Pedersen VRF") + blinding_base = cls.cv.point_type(*cls.cv.curve.params.auxiliary_points.blinding_base) + + if generator_scalar: + value = generator_scalar + if value > half_order: + value -= order + points[pos] = cls.cv.point_type.generator_point() + scalars[pos] = value + pos += 1 + if blinding_scalar: + value = blinding_scalar + if value > half_order: + value -= order + points[pos] = blinding_base + scalars[pos] = value + pos += 1 + + folded_points = [point for point in points[:pos] if point is not None] + folded_scalars = scalars[:pos] + seen: set[int] = set() + has_duplicate = False + for point in folded_points: + point_id = id(point) + if point_id in seen: + has_duplicate = True + break + seen.add(point_id) + + if has_duplicate: + points_by_id: dict[int, CurvePoint] = {} + scalars_by_id: dict[int, int] = {} + for point, scalar in zip(folded_points, folded_scalars, strict=True): + scalar %= order + if scalar == 0: + continue + point_id = id(point) + if point_id in scalars_by_id: + scalars_by_id[point_id] = (scalars_by_id[point_id] + scalar) % order + else: + points_by_id[point_id] = point + scalars_by_id[point_id] = scalar + + folded_points = [] + folded_scalars = [] + for point_id, point in points_by_id.items(): + scalar = scalars_by_id[point_id] % order + if scalar > half_order: + scalar -= order + if scalar: + folded_points.append(point) + folded_scalars.append(scalar) + + return cls.cv.point_type.msm(folded_points, folded_scalars).is_identity() diff --git a/dot_ring/vrf/primitives.py b/dot_ring/vrf/primitives.py new file mode 100644 index 0000000..1a160ec --- /dev/null +++ b/dot_ring/vrf/primitives.py @@ -0,0 +1,174 @@ +"""Bandersnatch VRF spec section 1.6 procedures.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from dot_ring.curve.curve import CurveVariant +from dot_ring.curve.point import CurvePoint +from dot_ring.vrf.codec import dec_scalar_mod, enc_64, enc_point, enc_scalar +from dot_ring.vrf.domain import DomSep + +SECURITY_PARAMETER = 128 +CHALLENGE_LEN = SECURITY_PARAMETER // 8 + + +@dataclass(frozen=True) +class VrfIo: + input: CurvePoint + output: CurvePoint + + def encode(self) -> bytes: + return enc_point(self.input) + enc_point(self.output) + + +class VrfTranscript: + """Append-only transcript with counter-mode XOF squeezing; no absorb after squeeze.""" + + def __init__(self, label: bytes, hash_fn: Any) -> None: + self._hash_fn = hash_fn + self._absorbed = bytearray(label) + self._seed: bytes | None = None + self._squeeze_offset = 0 + + def copy(self) -> VrfTranscript: + other = VrfTranscript(b"", self._hash_fn) + other._absorbed = bytearray(self._absorbed) + other._seed = self._seed + other._squeeze_offset = self._squeeze_offset + return other + + def absorb(self, data: bytes) -> None: + if self._seed is not None: + raise ValueError("cannot absorb after squeeze") + self._absorbed.extend(data) + + def squeeze(self, size: int) -> bytes: + if self._seed is None: + self._seed = bytes(self._absorbed) + start = self._squeeze_offset + end = start + size + stream = squeeze_transcript_bytes(self._hash_fn, self._seed, end) + self._squeeze_offset = end + return stream[start:end] + + +def new_transcript(curve: CurveVariant) -> VrfTranscript: + return VrfTranscript(curve.curve.params.suite_id, curve.curve.params.hash_fn) + + +def _expanded_scalar_len(curve: CurveVariant, sec_bits: int = SECURITY_PARAMETER) -> int: + return (curve.curve.params.subgroup_order.bit_length() + sec_bits + 7) // 8 + + +def nonce(curve: CurveVariant, secret_scalar: int, transcript: VrfTranscript | None = None) -> int: + """Spec section 1.6.6: derive the nonce from `NonceExpand` then `Nonce`.""" + t = transcript.copy() if transcript is not None else new_transcript(curve) + + t_exp = t.copy() + t_exp.absorb(bytes([DomSep.NONCE_EXPAND])) + t_exp.absorb(enc_scalar(curve, secret_scalar)) + secret_hash = t_exp.squeeze(64) + + t.absorb(bytes([DomSep.NONCE])) + t.absorb(secret_hash) + nonce_scalar = dec_scalar_mod(curve, t.squeeze(_expanded_scalar_len(curve))) + if nonce_scalar == 0: + raise ValueError("nonce scalar is zero") + return nonce_scalar + + +def challenge(curve: CurveVariant, points: list[CurvePoint], transcript: VrfTranscript | None = None) -> int: + """Spec section 1.6.7: absorb `Challenge` and encoded points, then squeeze a scalar.""" + t = transcript.copy() if transcript is not None else new_transcript(curve) + t.absorb(bytes([DomSep.CHALLENGE])) + for point in points: + t.absorb(enc_point(point)) + return dec_scalar_mod(curve, t.squeeze(CHALLENGE_LEN)) + + +def point_to_hash(curve: CurveVariant, point: CurvePoint, size: int = 32) -> bytes: + """Spec section 1.6.3: derive the user-visible VRF hash from output point `O`.""" + t = new_transcript(curve) + t.absorb(bytes([DomSep.POINT_TO_HASH])) + t.absorb(enc_point(point)) + return t.squeeze(size) + + +def vrf_transcript( + curve: CurveVariant, + scheme: DomSep, + ios: list[VrfIo], + ad: bytes, +) -> tuple[VrfTranscript, VrfIo]: + """Spec section 1.6.5: bind scheme/I/O/AD and return the delinearized pair.""" + t, zs = vrf_transcript_scalars(curve, scheme, ios, ad) + if not ios: + zero = curve.point_type.identity() + return t, VrfIo(zero, zero) + if len(ios) == 1: + return t, ios[0] + + input_acc = curve.point_type.msm([io.input for io in ios], zs) + output_acc = curve.point_type.msm([io.output for io in ios], zs) + return t, VrfIo(input_acc, output_acc) + + +def vrf_transcript_scalars( + curve: CurveVariant, + scheme: DomSep, + ios: list[VrfIo], + ad: bytes, +) -> tuple[VrfTranscript, list[int]]: + t = new_transcript(curve) + t.absorb(bytes([scheme])) + t.absorb(enc_64(len(ios))) + for io in ios: + t.absorb(io.encode()) + t.absorb(enc_64(len(ad))) + t.absorb(ad) + return t, delinearization_scalars(curve, t, len(ios)) + + +def delinearization_scalars(curve: CurveVariant, transcript: VrfTranscript, count: int) -> list[int]: + """Spec section 1.6.4: fork, absorb `Delinearize`, return `z_0 = 1` then squeezed scalars.""" + if count <= 0: + return [] + + t = transcript.copy() + t.absorb(bytes([DomSep.DELINEARIZE])) + scalars = [1] + for _ in range(count - 1): + scalars.append(dec_scalar_mod(curve, t.squeeze(CHALLENGE_LEN))) + return scalars + + +def secret_from_seed_scalar(curve: CurveVariant, seed: bytes) -> int: + if len(seed) != 32: + raise ValueError("seed must be exactly 32 bytes") + base_secret = dec_scalar_mod(curve, seed) + counter = 0 + while True: + t = new_transcript(curve) + t.absorb(seed) + if counter: + t.absorb(bytes([counter])) + secret = nonce(curve, base_secret, t) + if secret != 0: + return secret + counter += 1 + if counter > 255: + raise RuntimeError("failed to derive non-zero secret scalar") + + +def squeeze_transcript_bytes(hash_fn: Any, absorbed: bytes, size: int) -> bytes: + hasher = hash_fn() + if hasher.name in {"shake_128", "shake_256"}: + hasher.update(absorbed) + return hasher.digest(size) + + seed = hash_fn(absorbed).digest() + block_size = len(seed) + block_count = (size + block_size - 1) // block_size + return b"".join(hash_fn(seed + counter.to_bytes(8, "little")).digest() for counter in range(block_count))[:size] diff --git a/dot_ring/vrf/ring/__init__.py b/dot_ring/vrf/ring/__init__.py index eb465dd..072ee0d 100644 --- a/dot_ring/vrf/ring/__init__.py +++ b/dot_ring/vrf/ring/__init__.py @@ -1,14 +1,9 @@ -from .batch_verifier import RingBatchVerifier -from .context import RingContext, RingRootBuilder from .members import Ring from .root import RingRoot from .vrf import RingVRF __all__ = [ "Ring", - "RingBatchVerifier", - "RingContext", "RingRoot", - "RingRootBuilder", "RingVRF", ] diff --git a/dot_ring/vrf/ring/batch_verifier.py b/dot_ring/vrf/ring/batch_verifier.py deleted file mode 100644 index ed4dd76..0000000 --- a/dot_ring/vrf/ring/batch_verifier.py +++ /dev/null @@ -1,262 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any - -from dot_ring.curve.point import CurvePoint -from dot_ring.ring_proof.pcs.utils import ( - LinearPcsVerification, - g1_to_blst, -) -from dot_ring.ring_proof.transcript.transcript import FiatShamirTranscript -from dot_ring.ring_proof.verify import Verify, linear_pcs_verifications -from dot_ring.vrf.pedersen.batch_verifier import PedersenBatchVerifier, _PedersenBatchItem -from dot_ring.vrf.transcript import VrfIo - -from .members import Ring -from .root import RingRoot - -if TYPE_CHECKING: - from .vrf import RingVRF - - -@dataclass -class _RingBatchItem: - curve: Any - pedersen: _PedersenBatchItem - pcs: Any - verifications: tuple[Any, Any] - - -@dataclass(frozen=True) -class _RingBatchContext: - ring: Ring - ring_root: RingRoot - fixed_cols_cmts: list[Any] - fixed_cols_blst: tuple[Any, Any, Any] - transcript_prefix: FiatShamirTranscript - seed_point: CurvePoint - domain: list[int] - padding_rows: int - omega: int - edwards_a: int - domain_size_inv: int - - @classmethod - def from_ring(cls, ring: Ring, ring_root: RingRoot, *, validate_ring_root: bool = True) -> _RingBatchContext: - if validate_ring_root and not ring_root.matches_ring(ring): - raise ValueError("ring root does not match ring") - fixed_cols_cmts = ring_root.fixed_commitments(ring.params) - transcript_prefix = ring_root.verifier_transcript_prefix(ring.params) - domain = ring.params.domain - return cls( - ring=ring, - ring_root=ring_root, - fixed_cols_cmts=fixed_cols_cmts, - fixed_cols_blst=tuple(g1_to_blst(commitment) for commitment in fixed_cols_cmts), - transcript_prefix=transcript_prefix, - seed_point=ring.params.cv.point(ring.params.cv.curve.params.auxiliary_points.accumulator_base), - domain=domain, - padding_rows=ring.params.padding_rows, - omega=ring.params.omega, - edwards_a=ring.params.cv.curve.params.a, - domain_size_inv=pow(ring.params.domain_size, -1, ring.params.prime), - ) - - @property - def key(self) -> tuple[int, int]: - return (id(self.ring), id(self.ring_root)) - - def _decode_message_point(self, message: Any) -> Any: - if not isinstance(message, bytes): - return message - try: - return self.ring.params.cv.string_to_point(message) - except ValueError as exc: - raise ValueError("Invalid message point") from exc - - def ring_proof_verifier(self, proof: RingVRF, message: Any) -> Verify: - ring = self.ring - message = self._decode_message_point(message) - - rltn, res_plus_seed = _proof_relation_points(proof, message, ring.params) - witness_commitments, quotient_commitment = _proof_transcript_commitments(proof, ring.params) - - return Verify( - proof.ring_proof_tuple(), - self.fixed_cols_cmts, - rltn, - res_plus_seed, - self.seed_point, - self.domain, - self.transcript_prefix, - padding_rows=self.padding_rows, - edwards_a=self.edwards_a, - prime=ring.params.prime, - omega=self.omega, - pcs=ring.params.pcs, - domain_size_inv=self.domain_size_inv, - transcript_witness_commitments=witness_commitments, - transcript_quotient_commitment=quotient_commitment, - ) - - def ring_pcs_verifications(self, proof: RingVRF, message: Any) -> tuple[LinearPcsVerification, LinearPcsVerification]: - ring = self.ring - message = self._decode_message_point(message) - - rltn, res_plus_seed = _proof_relation_points(proof, message, ring.params) - witness_commitments, quotient_commitment = _proof_transcript_commitments(proof, ring.params) - return linear_pcs_verifications( - proof.as_ring_proof(), - self.fixed_cols_blst, - rltn, - res_plus_seed, - self.seed_point, - self.domain, - self.padding_rows, - self.domain_size_inv, - self.edwards_a, - self.omega, - ring.params.prime, - self.transcript_prefix, - witness_commitments, - quotient_commitment, - ) - - def _item_from_ios(self, proof: RingVRF, ios: list[VrfIo], ad: bytes) -> _RingBatchItem: - return _ring_batch_item(proof, ios, ad, self) - - def _item(self, proof: RingVRF, input: bytes, ad: bytes) -> _RingBatchItem: - pedersen_proof = proof.pedersen_proof - input_point = proof.cv.encode_to_curve(input) - return _ring_batch_item_from_trusted_ios(proof, [VrfIo(input_point, pedersen_proof.output_point)], ad, self) - - def _pedersen_item(self, proof: RingVRF, ios: list[VrfIo], ad: bytes) -> Any: - pedersen_proof = proof.pedersen_proof - if self.ring.params.cv.name != proof.cv.name: - raise ValueError("proof curve does not match ring curve") - verifier = PedersenBatchVerifier(proof.cv) - item = verifier._item(ios, ad, pedersen_proof) - if verifier._invalid: - raise ValueError("invalid Pedersen VRF point") - return item - - def _pedersen_item_from_trusted_ios(self, proof: RingVRF, ios: list[VrfIo], ad: bytes) -> Any: - pedersen_proof = proof.pedersen_proof - if self.ring.params.cv.name != proof.cv.name: - raise ValueError("proof curve does not match ring curve") - verifier = PedersenBatchVerifier(proof.cv) - item = verifier._item_from_trusted_ios(ios, ad, pedersen_proof) - if verifier._invalid: - raise ValueError("invalid Pedersen VRF point") - return item - - def _pcs_item(self, proof: RingVRF, message: Any) -> tuple[Any, tuple[LinearPcsVerification, LinearPcsVerification]]: - pcs = self.ring.params.pcs - # Deferred linear KZG verification avoids materializing small commitments before batching. - return pcs, self.ring_pcs_verifications(proof, message) - - -def _ring_batch_item(proof: RingVRF, ios: list[VrfIo], ad: bytes, context: _RingBatchContext) -> _RingBatchItem: - pedersen_item = context._pedersen_item(proof, ios, ad) - pcs, verifications = context._pcs_item(proof, proof.pedersen_proof.blinded_pk) - return _RingBatchItem(proof.cv, pedersen_item, pcs, verifications) - - -def _ring_batch_item_from_trusted_ios(proof: RingVRF, ios: list[VrfIo], ad: bytes, context: _RingBatchContext) -> _RingBatchItem: - pedersen_item = context._pedersen_item_from_trusted_ios(proof, ios, ad) - pcs, verifications = context._pcs_item(proof, proof.pedersen_proof.blinded_pk) - return _RingBatchItem(proof.cv, pedersen_item, pcs, verifications) - - -def _proof_transcript_commitments(proof: RingVRF, params: Any) -> tuple[bytes, Any]: - commitments = ( - proof.c_b.commitment, - proof.c_accip.commitment, - proof.c_accx.commitment, - proof.c_accy.commitment, - proof.c_q.commitment, - ) - transcript_commitments = tuple(params.pcs.serialize_g1_uncompressed(commitment) for commitment in commitments) - witness_commitments = b"".join(transcript_commitments[:4]) - quotient_commitment = transcript_commitments[4] - return witness_commitments, quotient_commitment - - -def _proof_relation_points(proof: RingVRF, message: Any, params: Any) -> tuple[CurvePoint, CurvePoint]: - relation = params.cv.point(message) - seed = params.cv.point(params.cv.curve.params.auxiliary_points.accumulator_base) - return relation, seed + relation - - -class RingBatchVerifier: - def __init__(self) -> None: - self.items: list[_RingBatchItem] = [] - self.pedersen_batches: dict[str, PedersenBatchVerifier] = {} - self.pcs_batches: dict[int, tuple[Any, list[Any]]] = {} - self._contexts: dict[tuple[int, int], _RingBatchContext] = {} - self._invalid = False - - def _push_item(self, item: _RingBatchItem) -> None: - self.items.append(item) - pedersen_batch = self.pedersen_batches.setdefault(item.curve.name, PedersenBatchVerifier(item.curve)) - pedersen_batch._push_item(item.pedersen) - _, verifications = self.pcs_batches.setdefault(id(item.pcs), (item.pcs, [])) - verifications.extend(item.verifications) - - def push_ios( - self, - proof: RingVRF, - ios: list[VrfIo], - ad: bytes, - ring: Ring, - ring_root: RingRoot, - *, - validate_ring_root: bool = True, - ) -> None: - try: - root_key = (id(ring), id(ring_root)) - context = self._contexts.get(root_key) - if context is None: - context = _RingBatchContext.from_ring(ring, ring_root, validate_ring_root=validate_ring_root) - self._contexts[root_key] = context - self._push_item(context._item_from_ios(proof, ios, ad)) - except (AssertionError, TypeError, ValueError): - self._invalid = True - - def push( - self, - proof: RingVRF, - input: bytes, - ad: bytes, - ring: Ring, - ring_root: RingRoot, - *, - validate_ring_root: bool = True, - ) -> None: - try: - root_key = (id(ring), id(ring_root)) - context = self._contexts.get(root_key) - if context is None: - context = _RingBatchContext.from_ring(ring, ring_root, validate_ring_root=validate_ring_root) - self._contexts[root_key] = context - self._push_item(context._item(proof, input, ad)) - except (AssertionError, TypeError, ValueError): - self._invalid = True - - def verify(self) -> bool: - if self._invalid: - return False - if not self.items: - return True - - if not all(batch.verify() for batch in self.pedersen_batches.values()): - return False - - try: - return all( - pcs.batch_verify_linear_preconverted(verifications) - for pcs, verifications in self.pcs_batches.values() - ) - except (AssertionError, TypeError, ValueError): - return False diff --git a/dot_ring/vrf/ring/context.py b/dot_ring/vrf/ring/context.py deleted file mode 100644 index 77e9eae..0000000 --- a/dot_ring/vrf/ring/context.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import annotations - -from collections.abc import Sequence -from dataclasses import dataclass, field - -from dot_ring.curve.curve import CurveVariant -from dot_ring.ring_proof.params import RingProofParams - -from .members import Ring, parse_concatenated_keys -from .root import RingRoot - - -@dataclass(frozen=True) -class RingContext: - params: RingProofParams - - @classmethod - def from_ring_size(cls, ring_size: int, *, test_vectors: bool = False, cv: CurveVariant | None = None) -> RingContext: - default_params = RingProofParams() - return cls(RingProofParams.from_ring_size(ring_size, test_vectors=test_vectors, cv=cv or default_params.cv)) - - @property - def max_ring_size(self) -> int: - return self.params.max_ring_size - - def ring(self, keys: Sequence[bytes | str] | bytes) -> Ring: - if isinstance(keys, bytes): - return Ring(parse_concatenated_keys(keys, self.params.cv), self.params) - return Ring(keys, self.params) - - def ring_root(self, ring: Ring | Sequence[bytes | str] | bytes) -> RingRoot: - if not isinstance(ring, Ring): - ring = self.ring(ring) - return RingRoot.from_ring(ring, self.params) - - def decode_ring_root(self, commitment: RingRoot | bytes) -> RingRoot: - if isinstance(commitment, RingRoot): - return commitment - return RingRoot.decode(commitment, self.params) - - def ring_root_builder(self) -> RingRootBuilder: - return RingRootBuilder(self) - - -@dataclass -class RingRootBuilder: - context: RingContext - keys: list[bytes] = field(default_factory=list) - _root: RingRoot | None = field(default=None, init=False, repr=False) - - def push(self, key: bytes) -> None: - self.extend([key]) - - def append(self, keys: list[bytes] | bytes) -> None: - self.extend(keys) - - def extend(self, keys: list[bytes] | bytes) -> None: - if isinstance(keys, bytes): - keys = parse_concatenated_keys(keys, self.context.params.cv) - if len(self.keys) + len(keys) > self.context.max_ring_size: - raise ValueError("too many keys for ring root builder") - self.keys.extend(keys) - self._root = None - if len(self.keys) == self.context.max_ring_size: - self._root = self.context.ring_root(self.keys) - - def finalize(self) -> RingRoot: - if self._root is None: - self._root = self.context.ring_root(self.keys) - return self._root diff --git a/dot_ring/vrf/ring/members.py b/dot_ring/vrf/ring/members.py index 23223ef..89154a6 100644 --- a/dot_ring/vrf/ring/members.py +++ b/dot_ring/vrf/ring/members.py @@ -10,7 +10,7 @@ from dot_ring.curve.curve import CurveVariant from dot_ring.ring_proof.params import RingProofParams -from dot_ring.vrf.transcript import point_len +from dot_ring.vrf.codec import point_len def parse_concatenated_keys(keys: bytes, cv: CurveVariant) -> list[bytes]: @@ -31,6 +31,8 @@ def __init__(self, keys: Sequence[bytes | str], params: RingProofParams | None = self.params = params padding_point = params.cv.curve.params.auxiliary_points.padding_point + if not padding_point: + raise ValueError("padding point is not configured in curve parameters") if len(keys) > params.max_ring_size: raise ValueError(f"ring size {len(keys)} exceeds max supported size {params.max_ring_size}") @@ -58,7 +60,7 @@ def _decode_key(self, key: object, padding_point: tuple[int, int]) -> tuple[int, if not isinstance(key, (bytes, str)): return None try: - point = self.params.cv.string_to_point(key) + point = self.params.cv.point_type.string_to_point(key) except ValueError: return None if point.is_identity(): @@ -83,6 +85,7 @@ def blinding_base_powers(params: RingProofParams, count: int) -> tuple[tuple[int points = [] for _ in range(count): points.append(point) - doubled = params.cv.point(point) + params.cv.point(point) + point_obj = params.cv.point_type(*point) + doubled = point_obj + point_obj point = int(doubled.x), int(doubled.y) return tuple(points) diff --git a/dot_ring/vrf/ring/root.py b/dot_ring/vrf/ring/root.py index 26433ea..4c33346 100644 --- a/dot_ring/vrf/ring/root.py +++ b/dot_ring/vrf/ring/root.py @@ -11,8 +11,8 @@ from .members import Ring -# Global bounded cache: selector columns are fixed for a params/PCS tuple. -@lru_cache(maxsize=16) +# Note: [Assumption] Selector column would likely be same as long as ring size doesn't change +@lru_cache(maxsize=4) def _selector_column_data( domain_size: int, max_ring_size: int, @@ -37,19 +37,7 @@ def _selector_column(params: RingProofParams) -> Column: params.prime, params.pcs, ) - return Column("s", list(evals), list(coeffs), commitment=_copy_commitment(commitment), size=params.domain_size) - - -def _copy_commitment(commitment: Any) -> Any: - return commitment.dup() if hasattr(commitment, "dup") else commitment - - -def _commitment_column(name: str, commitment: Any) -> Column: - return Column(name=name, evals=[], commitment=_copy_commitment(commitment)) - - -def _transcript_g2_points(g2_points: Any) -> list[tuple[int, int]]: - return [(b, a) for pair in g2_points for point in pair for a, b in [point]] + return Column("s", list(evals), list(coeffs), commitment=commitment.dup(), size=params.domain_size) @dataclass @@ -97,7 +85,7 @@ def verifier_transcript_prefix(self, params: RingProofParams | None = None, tran commitment_bytes = [params.pcs.serialize_g1_uncompressed(commitment) for commitment in commitments] verifier_key_bytes = serialize_verifier_key( params.pcs.srs.g1_points[0], - _transcript_g2_points(params.pcs.srs.g2_points), + [(b, a) for pair in params.pcs.srs.g2_points for point in pair for a, b in [point]], commitment_bytes, ) if transcript_challenge is None: @@ -136,9 +124,14 @@ def decode(cls, data: bytes, params: RingProofParams | None = None) -> "RingRoot s_commitment = reader.g1(pcs) reader.finish() - px = _commitment_column("px", px_commitment) - py = _commitment_column("py", py_commitment) - s = _commitment_column("s", s_commitment) + if params is None: + px = Column("px", [], commitment=px_commitment) + py = Column("py", [], commitment=py_commitment) + s = Column("s", [], commitment=s_commitment) + else: + px = Column("px", [], commitment=px_commitment, size=params.domain_size) + py = Column("py", [], commitment=py_commitment, size=params.domain_size) + s = Column("s", [], commitment=s_commitment, size=params.domain_size) return cls(px=px, py=py, s=s, params=params) diff --git a/dot_ring/vrf/ring/vrf.py b/dot_ring/vrf/ring/vrf.py index 5c30427..6bdffb0 100644 --- a/dot_ring/vrf/ring/vrf.py +++ b/dot_ring/vrf/ring/vrf.py @@ -6,6 +6,7 @@ from __future__ import annotations +from collections.abc import Sequence from dataclasses import dataclass from typing import Any @@ -13,16 +14,15 @@ from dot_ring.ring_proof.columns.columns import Column from dot_ring.ring_proof.params import RingProofParams from dot_ring.ring_proof.pcs.protocol import G1Commitment -from dot_ring.ring_proof.verify import RingProofFields, Verify +from dot_ring.ring_proof.pcs.utils import g1_to_blst +from dot_ring.ring_proof.proof_builder import RingProofBuilder +from dot_ring.ring_proof.proof_payload import RingProofPayload +from dot_ring.ring_proof.verify import RingProofFields, Verify, linear_pcs_verifications from dot_ring.vrf.pedersen import PedersenVRF from ..vrf import VRF -from .batch_verifier import RingBatchVerifier, _proof_relation_points, _proof_transcript_commitments -from .context import RingContext, RingRootBuilder from .members import Ring from .members import parse_concatenated_keys as _parse_concatenated_keys -from .proof_builder import RingProofBuilder -from .proof_payload import RingProofPayload from .root import RingRoot @@ -142,11 +142,13 @@ def _ring_proof_verifier( if isinstance(message, bytes): try: - message = ring.params.cv.string_to_point(message) + message = ring.params.cv.point_type.string_to_point(message) except ValueError as exc: raise ValueError("Invalid message point") from exc - rltn, res_plus_seed = _proof_relation_points(self, message, ring.params) + seed_point = ring.params.cv.point_type(*ring.params.cv.curve.params.auxiliary_points.accumulator_base) + rltn = message + res_plus_seed = seed_point + rltn witness_commitments, quotient_commitment = _proof_transcript_commitments(self, ring.params) return Verify( @@ -154,7 +156,7 @@ def _ring_proof_verifier( fixed_cols_cmts, rltn, res_plus_seed, - ring.params.cv.point(ring.params.cv.curve.params.auxiliary_points.accumulator_base), + seed_point, ring.params.domain, transcript_prefix, padding_rows=ring.params.padding_rows, @@ -182,17 +184,18 @@ def verify_ring_proof( def prove( cls, alpha: bytes, - ad: bytes, + additional_data: bytes, secret_key: bytes, producer_key: bytes, ring: Ring, ring_root: RingRoot | None = None, + salt: bytes = b"", ) -> RingVRF: """Spec section 5.1: run Pedersen prove, then prove `Y_bar = producer_key + b*B` is in the ring.""" if producer_key != cls.cv.public_key_from_secret(secret_key): raise ValueError("producer_key does not match secret_key") - pedersen_proof = PedersenVRF[cls.cv].prove(alpha, secret_key, ad) + pedersen_proof = PedersenVRF[cls.cv].prove(alpha, secret_key, additional_data, salt) ring_proof = RingProofBuilder( cls.cv, @@ -228,10 +231,79 @@ def verify(self, input: bytes, ad_data: bytes, ring: Ring, ring_root: RingRoot) def proof_to_hash(cls, gamma: CurvePoint, mul_cofactor: bool = False) -> bytes: return PedersenVRF[cls.cv].proof_to_hash(gamma, mul_cofactor) + @classmethod + def batch_verify( + cls, + proofs: Sequence[RingVRF], + inputs: Sequence[bytes], + additional_data: Sequence[bytes], + ring: Ring, + ring_root: RingRoot, + ) -> bool: + if not ring_root.matches_ring(ring): + return False + if ring.params.cv.name != cls.cv.name: + return False + + try: + pedersen_proofs = [] + for proof in proofs: + if proof.cv.name != cls.cv.name: + return False + pedersen_proofs.append(proof.pedersen_proof) + except (AttributeError, TypeError): + return False + + if not PedersenVRF[cls.cv].batch_verify(pedersen_proofs, inputs, additional_data): + return False + if not pedersen_proofs: + return True + + fixed_cols_cmts = ring_root.fixed_commitments(ring.params) + fixed_cols_blst = tuple(g1_to_blst(commitment) for commitment in fixed_cols_cmts) + transcript_prefix = ring_root.verifier_transcript_prefix(ring.params) + if not ring.params.cv.curve.params.auxiliary_points.accumulator_base: + raise ValueError("Curve does not have an accumulator base point for Ring VRF") + seed_point = ring.params.cv.point_type(*ring.params.cv.curve.params.auxiliary_points.accumulator_base) + domain = ring.params.domain + domain_size_inv = pow(ring.params.domain_size, -1, ring.params.prime) + pcs_verifications: list[Any] = [] + try: + for proof in proofs: + relation = proof.pedersen_proof.blinded_pk + result_plus_seed = seed_point + relation + witness_commitments, quotient_commitment = _proof_transcript_commitments(proof, ring.params) + pcs_verifications.extend( + linear_pcs_verifications( + proof.as_ring_proof(), + fixed_cols_blst, + relation, + result_plus_seed, + seed_point, + domain, + ring.params.padding_rows, + domain_size_inv, + ring.params.cv.curve.params.a, + ring.params.omega, + ring.params.prime, + transcript_prefix, + witness_commitments, + quotient_commitment, + ) + ) + except (AssertionError, AttributeError, TypeError, ValueError): + return False + + return bool(ring.params.pcs.batch_verify_linear_preconverted(pcs_verifications)) + -__all__ = [ - "RingBatchVerifier", - "RingContext", - "RingVRF", - "RingRootBuilder", -] +def _proof_transcript_commitments(proof: RingVRF, params: RingProofParams) -> tuple[bytes, Any]: + commitments = ( + proof.c_b.commitment, + proof.c_accip.commitment, + proof.c_accx.commitment, + proof.c_accy.commitment, + proof.c_q.commitment, + ) + transcript_commitments = tuple(params.pcs.serialize_g1_uncompressed(commitment) for commitment in commitments) + return b"".join(transcript_commitments[:4]), transcript_commitments[4] diff --git a/dot_ring/vrf/transcript.py b/dot_ring/vrf/transcript.py deleted file mode 100644 index 65dc0d7..0000000 --- a/dot_ring/vrf/transcript.py +++ /dev/null @@ -1,238 +0,0 @@ -"""VRF transcript primitives from Bandersnatch VRF spec sections 1.5-1.9. - -The public helpers intentionally keep spec names: `vrf_transcript`, `nonce`, -`challenge`, and `point_to_hash`. -""" - -from __future__ import annotations - -from typing import Any - -from dot_ring.curve.curve import CurveVariant -from dot_ring.curve.point import CurvePoint -from dot_ring.vrf.domain import DomSep -from dot_ring.vrf.io import VrfIo - -SECURITY_PARAMETER = 128 -CHALLENGE_LEN = SECURITY_PARAMETER // 8 - - -class VrfTranscript: - """Append-only transcript with counter-mode XOF squeezing; no absorb after squeeze.""" - - def __init__(self, label: bytes, hash_fn: Any) -> None: - self._hash_fn = hash_fn - self._absorbed = bytearray(label) - self._seed: bytes | None = None - self._squeeze_offset = 0 - - def copy(self) -> VrfTranscript: - other = VrfTranscript(b"", self._hash_fn) - other._absorbed = bytearray(self._absorbed) - other._seed = self._seed - other._squeeze_offset = self._squeeze_offset - return other - - def absorb(self, data: bytes) -> None: - if self._seed is not None: - raise ValueError("cannot absorb after squeeze") - self._absorbed.extend(data) - - def squeeze(self, size: int) -> bytes: - if self._seed is None: - self._seed = bytes(self._absorbed) - start = self._squeeze_offset - end = start + size - stream = squeeze_transcript_bytes(self._hash_fn, self._seed, end) - self._squeeze_offset = end - return stream[start:end] - - -def suite_id(curve: CurveVariant) -> bytes: - return curve.curve.params.suite_id - - -def scalar_len(curve: CurveVariant) -> int: - return (curve.curve.params.subgroup_order.bit_length() + 7) // 8 - - -def point_len(curve: CurveVariant) -> int: - encoding = curve.curve.params.encoding - return encoding.point_len * (2 if encoding.uncompressed else 1) - - -def scalar_encode(curve: CurveVariant, value: int) -> bytes: - return int(value % curve.curve.params.subgroup_order).to_bytes(scalar_len(curve), "little") - - -def scalar_decode(curve: CurveVariant, value: bytes) -> int: - return int.from_bytes(value, "little") - - -def _expanded_scalar_len(curve: CurveVariant, sec_bits: int = SECURITY_PARAMETER) -> int: - return (curve.curve.params.subgroup_order.bit_length() + sec_bits + 7) // 8 - - -def _transcript_for(curve: CurveVariant) -> VrfTranscript: - return VrfTranscript(suite_id(curve), curve.curve.params.hash_fn) - - -def _nonce_scalar(curve: CurveVariant, transcript: VrfTranscript) -> int: - return int.from_bytes(transcript.squeeze(_expanded_scalar_len(curve)), "little") % curve.curve.params.subgroup_order - - -def _challenge_scalar(curve: CurveVariant, transcript: VrfTranscript) -> int: - return int.from_bytes(transcript.squeeze(CHALLENGE_LEN), "little") % curve.curve.params.subgroup_order - - -def nonce(curve: CurveVariant, secret_scalar: int, transcript: VrfTranscript | None = None) -> int: - """Spec section 1.8: derive the nonce from `NonceExpand` then `Nonce`; abort on zero.""" - t = transcript.copy() if transcript is not None else _transcript_for(curve) - - t_exp = t.copy() - t_exp.absorb(bytes([DomSep.NONCE_EXPAND])) - t_exp.absorb(scalar_encode(curve, secret_scalar)) - secret_hash = t_exp.squeeze(64) - - t.absorb(bytes([DomSep.NONCE])) - t.absorb(secret_hash) - nonce_scalar = _nonce_scalar(curve, t) - if nonce_scalar == 0: - raise ValueError("nonce scalar is zero") - return nonce_scalar - - -def challenge(curve: CurveVariant, points: list[CurvePoint], transcript: VrfTranscript | None = None) -> int: - """Spec section 1.9: absorb `Challenge` and encoded points, then squeeze challenge_len.""" - t = transcript.copy() if transcript is not None else _transcript_for(curve) - t.absorb(bytes([DomSep.CHALLENGE])) - for point in points: - t.absorb(point.point_to_string()) - return _challenge_scalar(curve, t) - - -def point_to_hash(curve: CurveVariant, point: CurvePoint, size: int = 32) -> bytes: - """Spec section 1.4: domain-separate `O` before producing the user-visible VRF hash.""" - t = _transcript_for(curve) - t.absorb(bytes([DomSep.POINT_TO_HASH])) - t.absorb(point.point_to_string()) - return t.squeeze(size) - - -def vrf_transcript( - curve: CurveVariant, - scheme: DomSep, - ios: list[VrfIo], - ad: bytes, -) -> tuple[VrfTranscript, VrfIo]: - """Spec section 1.7: bind scheme/I/O/AD and return the delinearized merged pair.""" - t, zs = vrf_transcript_scalars(curve, scheme, ios, ad) - if not ios: - zero = curve.identity() - return t, VrfIo(zero, zero) - if len(ios) == 1: - return t, ios[0] - - input_acc = curve.msm([io.input for io in ios], zs) - output_acc = curve.msm([io.output for io in ios], zs) - return t, VrfIo(input_acc, output_acc) - - -def vrf_transcript_scalars( - curve: CurveVariant, - scheme: DomSep, - ios: list[VrfIo], - ad: bytes, -) -> tuple[VrfTranscript, list[int]]: - """Spec section 1.7 plus section 1.6 delinearization scalars for these I/O pairs.""" - t = _transcript_for(curve) - t.absorb(bytes([scheme])) - t.absorb(len(ios).to_bytes(8, "little")) - for io in ios: - t.absorb(io.encode()) - t.absorb(len(ad).to_bytes(8, "little")) - t.absorb(ad) - return t, delinearization_scalars(curve, t, len(ios)) - - -def delinearization_scalars(curve: CurveVariant, transcript: VrfTranscript, count: int) -> list[int]: - """Spec section 1.6: fork, absorb `Delinearize`, return `z_0 = 1` then squeezed scalars.""" - if count <= 0: - return [] - - t = transcript.copy() - t.absorb(bytes([DomSep.DELINEARIZE])) - order = curve.curve.params.subgroup_order - scalars = [1] - for _ in range(count - 1): - scalars.append(int.from_bytes(t.squeeze(CHALLENGE_LEN), "little") % order) - return scalars - - -def schnorr_ios(curve: CurveVariant, public_key: CurvePoint, ios: list[VrfIo]) -> list[VrfIo]: - generator = curve.generator_point() - return [VrfIo(generator, public_key), *ios] - - -def secret_from_seed_scalar(curve: CurveVariant, seed: bytes) -> int: - if len(seed) != 32: - raise ValueError("seed must be exactly 32 bytes") - base_secret = int.from_bytes(seed, "little") % curve.curve.params.subgroup_order - counter = 0 - while True: - t = _transcript_for(curve) - t.absorb(seed) - if counter: - t.absorb(bytes([counter])) - secret = nonce(curve, base_secret, t) - if secret != 0: - return secret - counter += 1 - if counter > 255: - raise RuntimeError("failed to derive non-zero secret scalar") - - -def squeeze_transcript_bytes(hash_fn: Any, absorbed: bytes, size: int) -> bytes: - hasher = hash_fn() - if hasher.name in {"shake_128", "shake_256"}: - hasher.update(absorbed) - return hasher.digest(size) - - seed = hash_fn(absorbed).digest() - block_size = len(seed) - block_count = (size + block_size - 1) // block_size - return b"".join(hash_fn(seed + counter.to_bytes(8, "little")).digest() for counter in range(block_count))[:size] - - -def hash_to_curve_tai(point_type: Any, data: bytes, curve: Any) -> CurvePoint: - field_len = (curve.params.field_modulus.bit_length() + 7) // 8 - - prefix = VrfTranscript(curve.params.suite_id, curve.params.hash_fn) - prefix.absorb(bytes([DomSep.HASH_TO_CURVE])) - prefix.absorb(len(data).to_bytes(8, "little")) - prefix.absorb(data) - - for counter in range(256): - t = prefix.copy() - t.absorb(bytes([counter])) - candidate = bytearray(t.squeeze(field_len)) - if hasattr(curve.params, "a") and hasattr(curve.params, "b"): - shave = field_len * 8 - curve.params.field_modulus.bit_length() - if shave: - candidate[-1] &= (1 << (8 - shave)) - 1 - candidate = bytearray(candidate + b"\x80") - else: - sign = candidate[-1] & 0x80 - shave = field_len * 8 - curve.params.field_modulus.bit_length() - if shave: - candidate[-1] &= (1 << (8 - shave)) - 1 - candidate[-1] |= sign - try: - point = point_type.string_to_point(bytes(candidate), curve) - except ValueError: - continue - if point.curve.params.cofactor > 1: - point = point * point.curve.params.cofactor - if not point.is_identity(): - return point - raise ValueError("hash_to_curve_tai failed") diff --git a/dot_ring/vrf/vrf.py b/dot_ring/vrf/vrf.py index 384f65d..7b57153 100644 --- a/dot_ring/vrf/vrf.py +++ b/dot_ring/vrf/vrf.py @@ -1,44 +1,17 @@ from __future__ import annotations -from functools import lru_cache from typing import Any, ClassVar, Generic, Self, TypeVar from ..curve.curve import CurveVariant -from ..curve.point import CurvePoint -from ..curve.twisted_edwards.te_affine_point import TEAffinePoint C = TypeVar("C", bound=CurveVariant) -@lru_cache(maxsize=32) -def _cofactor_inverse(cofactor: int, subgroup_order: int) -> int: - return pow(cofactor, -1, subgroup_order) - - class VRF(Generic[C]): """Base VRF class that specializes implementations by curve variant.""" cv: ClassVar[CurveVariant] - @staticmethod - def _valid_point(point: CurvePoint) -> bool: - """Verifier-side `dec_point`: nonidentity and in the prime-order subgroup.""" - if point.is_identity(): - return False - - cofactor = point.curve.params.cofactor - if cofactor == 1: - return True - - cofactor_inv = _cofactor_inverse(cofactor, point.curve.params.subgroup_order) - if isinstance(point, TEAffinePoint): - cleared = TEAffinePoint.__mul__(point, cofactor) - else: - cleared = point * cofactor - if cleared.is_identity(): - return False - return cleared * cofactor_inv == point - def __class_getitem__(cls, curve_variant: CurveVariant | Any) -> type[Self] | Any: """ Create a specialized VRF class for a specific curve variant. @@ -53,3 +26,21 @@ def __class_getitem__(cls, curve_variant: CurveVariant | Any) -> type[Self] | An return cls new_class = type(f"{cls.__name__}[{curve_variant.name}]", (cls,), {"cv": curve_variant}) return new_class + + @classmethod + def prove(cls, alpha: bytes, secret_key: bytes, additional_data: bytes, salt: bytes, **_kwargs: Any) -> Self: + raise NotImplementedError(f"{cls.__name__} does not implement prove") + + def verify(self, *_args: Any, **_kwargs: Any) -> bool: + raise NotImplementedError(f"{self.__class__.__name__} does not implement verify") + + def encode(self) -> bytes: + raise NotImplementedError(f"{self.__class__.__name__} does not implement encode") + + @classmethod + def decode(cls, data: bytes) -> Self: + raise NotImplementedError(f"{cls.__name__} does not implement from_bytes") + + @classmethod + def batch_verify(cls, *_args: Any, **_kwargs: Any) -> bool: + raise NotImplementedError(f"{cls.__name__} does not implement batch_verify") diff --git a/scripts/generate_test_vectors.py b/scripts/generate_test_vectors.py index bbf4298..a9cbb21 100644 --- a/scripts/generate_test_vectors.py +++ b/scripts/generate_test_vectors.py @@ -17,10 +17,10 @@ from dot_ring.curve.specs.jubjub import JubJub from dot_ring.curve.specs.p256 import P256_TAI from dot_ring.ring_proof.params import RingProofParams +from dot_ring.vrf.codec import enc_scalar, point_len, scalar_len from dot_ring.vrf.ietf import ThinVRF, TinyVRF from dot_ring.vrf.pedersen import PedersenVRF from dot_ring.vrf.ring import Ring, RingRoot, RingVRF -from dot_ring.vrf.transcript import point_len, scalar_encode, scalar_len DEFAULT_OUT_DIR = Path(__file__).resolve().parent.parent / "tests" / "vectors" / "dot-ring" RING_SIZE = 8 @@ -72,7 +72,7 @@ def pedersen_len(curve: CurveVariant) -> int: def base_fields(suite: Suite, scheme: str, index: int, seed: int, alpha: bytes, ad: bytes) -> tuple[bytes, dict[str, str]]: pk, sk = suite.curve.secret_from_seed(seed_bytes(seed)) - input_point = suite.curve.encode_to_curve(alpha) + input_point = suite.curve.point_type.encode_to_curve(alpha) gamma = input_point * int.from_bytes(sk, "little") beta = TinyVRF[suite.curve].proof_to_hash(gamma) return sk, { @@ -93,7 +93,7 @@ def tiny_vector(suite: Suite, index: int, seed: int, alpha: bytes, ad: bytes) -> vector.update( { "proof_c": proof.c.to_bytes(16, "little").hex(), - "proof_s": scalar_encode(suite.curve, proof.s).hex(), + "proof_s": enc_scalar(suite.curve, proof.s).hex(), } ) return vector @@ -105,7 +105,7 @@ def thin_vector(suite: Suite, index: int, seed: int, alpha: bytes, ad: bytes) -> vector.update( { "proof_r": proof.r.point_to_string().hex(), - "proof_s": scalar_encode(suite.curve, proof.s).hex(), + "proof_s": enc_scalar(suite.curve, proof.s).hex(), } ) return vector @@ -116,12 +116,12 @@ def pedersen_vector(suite: Suite, index: int, seed: int, alpha: bytes, ad: bytes proof = PedersenVRF[suite.curve].prove(alpha, sk, ad) vector.update( { - "blinding": scalar_encode(suite.curve, proof._blinding_factor).hex(), + "blinding": enc_scalar(suite.curve, proof._blinding_factor).hex(), "proof_pk_com": proof.blinded_pk.point_to_string().hex(), "proof_r": proof.result_point.point_to_string().hex(), "proof_ok": proof.ok.point_to_string().hex(), - "proof_s": scalar_encode(suite.curve, proof.s).hex(), - "proof_sb": scalar_encode(suite.curve, proof.sb).hex(), + "proof_s": enc_scalar(suite.curve, proof.s).hex(), + "proof_sb": enc_scalar(suite.curve, proof.sb).hex(), } ) return vector @@ -159,12 +159,12 @@ def ring_vector(suite: Suite, index: int, seed: int, alpha: bytes, ad: bytes) -> proof_bytes = proof.encode() vector.update( { - "blinding": scalar_encode(suite.curve, pedersen._blinding_factor).hex(), + "blinding": enc_scalar(suite.curve, pedersen._blinding_factor).hex(), "proof_pk_com": pedersen.blinded_pk.point_to_string().hex(), "proof_r": pedersen.result_point.point_to_string().hex(), "proof_ok": pedersen.ok.point_to_string().hex(), - "proof_s": scalar_encode(suite.curve, pedersen.s).hex(), - "proof_sb": scalar_encode(suite.curve, pedersen.sb).hex(), + "proof_s": enc_scalar(suite.curve, pedersen.s).hex(), + "proof_sb": enc_scalar(suite.curve, pedersen.sb).hex(), "ring_pks": b"".join(keys).hex(), "ring_pks_com": ring_root.encode().hex(), "ring_proof": proof_bytes[pedersen_len(suite.curve) :].hex(), diff --git a/tests/benchmark/bench_ring_proof.py b/tests/benchmark/bench_ring_proof.py index 5ec6f06..b4c6949 100644 --- a/tests/benchmark/bench_ring_proof.py +++ b/tests/benchmark/bench_ring_proof.py @@ -13,7 +13,7 @@ from dot_ring import Bandersnatch from dot_ring.ring_proof.params import RingProofParams -from dot_ring.vrf.ring import RingBatchVerifier, RingRoot, RingVRF +from dot_ring.vrf.ring import RingRoot, RingVRF from dot_ring.vrf.ring.members import Ring SIGNER_INDEX = 3 @@ -176,24 +176,11 @@ def _build_batch_fixtures_parallel(ring_size: int, fixture_count: int, workers: serialized.extend(future.result()) serialized.sort(key=lambda fixture: fixture.index) - fixtures = [ - ProofFixture( - RingVRF[Bandersnatch].decode(fixture.proof), - fixture.alpha, - fixture.ad, - ) - for fixture in serialized - ] + fixtures = [ProofFixture(RingVRF[Bandersnatch].decode(fixture.proof), fixture.alpha, fixture.ad) for fixture in serialized] return ring, ring_root, fixtures -def run( - ring_size: int, - samples: int, - max_batch_size: int, - batch_fixtures: int | None, - batch_workers: int, -) -> None: +def run(ring_size: int, samples: int, max_batch_size: int, batch_fixtures: int | None, batch_workers: int) -> None: if samples <= 0: raise ValueError("samples must be positive") if batch_fixtures is None: @@ -216,37 +203,37 @@ def run( _print_stats("total", [timing.total_ms for timing in timings]) print(f"proof_size: {timings[-1].proof_size} bytes") - fixture_count, workers = batch_fixtures, batch_workers if max_batch_size <= 0: return - if fixture_count <= 0: + if batch_fixtures <= 0: raise ValueError("batch fixture count must be positive") - if fixture_count < max_batch_size: + if batch_fixtures < max_batch_size: raise ValueError("batch fixture count must be at least batch max so every batch entry is unique") - if samples <= 0: - raise ValueError("batch samples must be positive") - if workers <= 0: + if batch_workers <= 0: raise ValueError("batch workers must be positive") - workers = min(workers, fixture_count) + workers = min(batch_workers, batch_fixtures) print() - print(f"Batch verify: max_n={max_batch_size}, unique_fixtures={fixture_count}") + print(f"Batch verify: max_n={max_batch_size}, unique_fixtures={batch_fixtures}") print(f"Preparing proofs... | workers={workers}") - ring, ring_root, fixtures = _build_batch_fixtures_parallel(ring_size, fixture_count, workers) + ring, ring_root, fixtures = _build_batch_fixtures_parallel(ring_size, batch_fixtures, workers) print() print("batch_n batch_ms batch_ms_per_proof") for batch_size in _batch_sizes(max_batch_size): - verifier = RingBatchVerifier() + batch = fixtures[:batch_size] start = time.perf_counter() - for fixture in fixtures[:batch_size]: - verifier.push(fixture.proof, fixture.alpha, fixture.ad, ring, ring_root) - verified = verifier.verify() + verified = RingVRF[Bandersnatch].batch_verify( + [fixture.proof for fixture in batch], + [fixture.alpha for fixture in batch], + [fixture.ad for fixture in batch], + ring, + ring_root, + ) elapsed_ms = (time.perf_counter() - start) * 1000 if not verified: raise AssertionError(f"batch verification failed for n={batch_size}") - print(f"{batch_size:>7} {elapsed_ms:>13.2f} {elapsed_ms / batch_size:>18.2f}") diff --git a/tests/test_ark_vrf.py b/tests/test_ark_vrf.py index e053d1e..7ff0fc9 100644 --- a/tests/test_ark_vrf.py +++ b/tests/test_ark_vrf.py @@ -11,10 +11,9 @@ from dot_ring.curve.specs.jubjub import JubJub from dot_ring.curve.specs.p256 import P256_TAI from dot_ring.ring_proof.params import RingProofParams -from dot_ring.vrf.ietf import ThinBatchVerifier, ThinVRF, TinyVRF -from dot_ring.vrf.pedersen import PedersenBatchVerifier, PedersenVRF -from dot_ring.vrf.ring import Ring, RingBatchVerifier, RingContext, RingRoot, RingVRF -from dot_ring.vrf.transcript import VrfIo +from dot_ring.vrf.ietf import ThinVRF, TinyVRF +from dot_ring.vrf.pedersen import PedersenVRF +from dot_ring.vrf.ring import Ring, RingRoot, RingVRF from scripts.generate_test_vectors import SUITES as GENERATED_SUITES from scripts.generate_test_vectors import scheme_vectors from tests.vector_helpers import bytes_from_fields, load_json_vectors, pedersen_proof_bytes, ring_proof_bytes, thin_proof_bytes, tiny_proof_bytes @@ -52,7 +51,7 @@ def test_tiny_vectors(curve, prefix: str, point_len: int) -> None: proof_bytes = proof.encode() assert curve.public_key_from_secret(sk).hex() == vector["pk"] - assert curve.encode_to_curve(alpha).point_to_string().hex() == vector["h"] + assert curve.point_type.encode_to_curve(alpha).point_to_string().hex() == vector["h"] assert proof_bytes == tiny_proof_bytes(vector) assert TinyVRF[curve].proof_to_hash(proof.output_point).hex() == vector["beta"] assert proof.verify(bytes.fromhex(vector["pk"]), alpha, ad) @@ -133,75 +132,40 @@ def test_ring_vectors(curve, filename: str) -> None: assert generated.encode() == proof_bytes -def test_multi_input_apis() -> None: - secret = bytes.fromhex(load("bandersnatch_sha-512_ell2_tiny.json")[0]["sk"]) - public_key = Bandersnatch.public_key_from_secret(secret) - secret_scalar = int.from_bytes(secret, "little") - public_key_point = Bandersnatch.string_to_point(public_key) - - ios = [] - for alpha in (b"first", b"second"): - input_point = Bandersnatch.encode_to_curve(alpha) - ios.append(VrfIo(input_point, input_point * secret_scalar)) - - tiny = TinyVRF[Bandersnatch].prove_ios(ios, secret_scalar, public_key_point, b"ad") - thin = ThinVRF[Bandersnatch].prove_ios(ios, secret_scalar, public_key_point, b"ad") - pedersen = PedersenVRF[Bandersnatch].prove_ios(ios, secret_scalar, public_key_point, b"ad") - - assert tiny.verify_ios(public_key_point, ios, b"ad") - assert thin.verify_ios(public_key_point, ios, b"ad") - assert pedersen.verify_ios(ios, b"ad") - - -def test_batch_apis() -> None: +def test_batch_verify_apis() -> None: vectors = load("bandersnatch_sha-512_ell2_thin.json")[:2] - thin_batch = ThinBatchVerifier[Bandersnatch]() - pedersen_batch = PedersenBatchVerifier[Bandersnatch]() - - for vector in vectors: - alpha = bytes.fromhex(vector["alpha"]) - ad = bytes.fromhex(vector["ad"]) - pk_point = Bandersnatch.string_to_point(bytes.fromhex(vector["pk"])) - thin = ThinVRF[Bandersnatch].prove(alpha, bytes.fromhex(vector["sk"]), ad) - input_point = Bandersnatch.encode_to_curve(alpha) - thin_batch.push(pk_point, [VrfIo(input_point, thin.output_point)], ad, thin) - - pedersen = PedersenVRF[Bandersnatch].prove(alpha, bytes.fromhex(vector["sk"]), ad) - pedersen_batch.push([VrfIo(input_point, pedersen.output_point)], ad, pedersen) - - assert thin_batch.verify() - assert pedersen_batch.verify() - - -def test_batch_rejects_invalid_items() -> None: - vector = load("bandersnatch_sha-512_ell2_thin.json")[0] - alpha = bytes.fromhex(vector["alpha"]) - ad = bytes.fromhex(vector["ad"]) - secret = bytes.fromhex(vector["sk"]) - pk_point = Bandersnatch.string_to_point(bytes.fromhex(vector["pk"])) - input_point = Bandersnatch.encode_to_curve(alpha) - - thin = ThinVRF[Bandersnatch].prove(alpha, secret, ad) - bad_thin = ThinVRF[Bandersnatch](thin.output_point, thin.r, (thin.s + 1) % Bandersnatch.curve.params.subgroup_order) - thin_batch = ThinBatchVerifier[Bandersnatch]() - thin_batch.push(pk_point, [VrfIo(input_point, bad_thin.output_point)], ad, bad_thin) - assert not thin_batch.verify() - - pedersen = PedersenVRF[Bandersnatch].prove(alpha, secret, ad) + thin_proofs = [ + ThinVRF[Bandersnatch].prove(bytes.fromhex(vector["alpha"]), bytes.fromhex(vector["sk"]), bytes.fromhex(vector["ad"])) for vector in vectors + ] + inputs = [bytes.fromhex(vector["alpha"]) for vector in vectors] + ads = [bytes.fromhex(vector["ad"]) for vector in vectors] + public_keys = [bytes.fromhex(vector["pk"]) for vector in vectors] + + assert ThinVRF[Bandersnatch].batch_verify(thin_proofs, public_keys, inputs, ads) + bad_thin = copy.copy(thin_proofs[0]) + bad_thin.s = (bad_thin.s + 1) % Bandersnatch.curve.params.subgroup_order + assert not ThinVRF[Bandersnatch].batch_verify([bad_thin, thin_proofs[1]], public_keys, inputs, ads) + + pedersen_vectors = load("bandersnatch_sha-512_ell2_pedersen.json")[:2] + pedersen_proofs = [ + PedersenVRF[Bandersnatch].prove(bytes.fromhex(vector["alpha"]), bytes.fromhex(vector["sk"]), bytes.fromhex(vector["ad"])) + for vector in pedersen_vectors + ] + pedersen_inputs = [bytes.fromhex(vector["alpha"]) for vector in pedersen_vectors] + pedersen_ads = [bytes.fromhex(vector["ad"]) for vector in pedersen_vectors] + assert PedersenVRF[Bandersnatch].batch_verify(pedersen_proofs, pedersen_inputs, pedersen_ads) bad_pedersen = PedersenVRF[Bandersnatch]( - pedersen.output_point, - pedersen.blinded_pk, - pedersen.result_point, - pedersen.ok, - (pedersen.s + 1) % Bandersnatch.curve.params.subgroup_order, - pedersen.sb, + pedersen_proofs[0].output_point, + pedersen_proofs[0].blinded_pk, + pedersen_proofs[0].result_point, + pedersen_proofs[0].ok, + (pedersen_proofs[0].s + 1) % Bandersnatch.curve.params.subgroup_order, + pedersen_proofs[0].sb, ) - pedersen_batch = PedersenBatchVerifier[Bandersnatch]() - pedersen_batch.push([VrfIo(input_point, bad_pedersen.output_point)], ad, bad_pedersen) - assert not pedersen_batch.verify() + assert not PedersenVRF[Bandersnatch].batch_verify([bad_pedersen, pedersen_proofs[1]], pedersen_inputs, pedersen_ads) -def test_ring_batch_same_ring_and_invalid_item() -> None: +def test_ring_batch_verify_api(monkeypatch: pytest.MonkeyPatch) -> None: vector = load("bandersnatch_sha-512_ell2_ring.json")[0] alpha = bytes.fromhex(vector["alpha"]) ad = bytes.fromhex(vector["ad"]) @@ -212,61 +176,21 @@ def test_ring_batch_same_ring_and_invalid_item() -> None: proof = RingVRF[Bandersnatch].decode(ring_proof_bytes(vector)) second_alpha = b"ring-batch-second" second_ad = b"ring-batch-ad" - second_proof = RingVRF[Bandersnatch].prove( - second_alpha, - second_ad, - bytes.fromhex(vector["sk"]), - bytes.fromhex(vector["pk"]), - ring, - ring_root, - ) - - batch = RingBatchVerifier() - batch.push(proof, alpha, ad, ring, ring_root) - batch.push(second_proof, second_alpha, second_ad, ring, ring_root) - assert batch.verify() - - bad_proof = copy.copy(second_proof) - bad_proof.l_zeta_omega = (bad_proof.l_zeta_omega + 1) % params.prime - bad_batch = RingBatchVerifier() - bad_batch.push(proof, alpha, ad, ring, ring_root) - bad_batch.push(bad_proof, second_alpha, second_ad, ring, ring_root) - assert not bad_batch.verify() - + second_proof = RingVRF[Bandersnatch].prove(second_alpha, second_ad, bytes.fromhex(vector["sk"]), bytes.fromhex(vector["pk"]), ring, ring_root) -def test_ring_batch_multi_ring_shared_srs(monkeypatch: pytest.MonkeyPatch) -> None: - vectors = load("bandersnatch_sha-512_ell2_ring.json")[:2] - batch = RingBatchVerifier() - - for vector in vectors: - keys = RingVRF[Bandersnatch].parse_keys(bytes.fromhex(vector["ring_pks"])) - params = RingProofParams(test_vectors=True) - ring = Ring(keys, params) - ring_root = RingRoot.from_ring(ring, params) - proof = RingVRF[Bandersnatch].decode(ring_proof_bytes(vector)) - batch.push(proof, bytes.fromhex(vector["alpha"]), bytes.fromhex(vector["ad"]), ring, ring_root) + batch = [proof, second_proof] + inputs = [alpha, second_alpha] + ads = [ad, second_ad] def fail_single_verify(*_args: object, **_kwargs: object) -> bool: - raise AssertionError("RingBatchVerifier must use batched checks") + raise AssertionError("RingVRF.batch_verify must use batched checks") monkeypatch.setattr(RingVRF, "verify", fail_single_verify) - assert batch.verify() - - -def test_ring_context_helpers() -> None: - vector = load("bandersnatch_sha-512_ell2_ring.json")[0] - keys_blob = bytes.fromhex(vector["ring_pks"]) - context = RingContext(RingProofParams(test_vectors=True)) + assert RingVRF[Bandersnatch].batch_verify(batch, inputs, ads, ring, ring_root) - ring = context.ring(keys_blob) - ring_root = context.ring_root(ring) - builder = context.ring_root_builder() - builder.extend(keys_blob) - - assert ring_root.encode().hex() == vector["ring_pks_com"] - assert context.ring_root(keys_blob).encode() == ring_root.encode() - assert context.decode_ring_root(ring_root.encode()).encode() == ring_root.encode() - assert builder.finalize().encode() == ring_root.encode() + bad_proof = copy.copy(second_proof) + bad_proof.l_zeta_omega = (bad_proof.l_zeta_omega + 1) % params.prime + assert not RingVRF[Bandersnatch].batch_verify([proof, bad_proof], inputs, ads, ring, ring_root) def test_negative_and_malformed_proofs() -> None: @@ -277,12 +201,8 @@ def test_negative_and_malformed_proofs() -> None: sk = bytes.fromhex(tiny_vector["sk"]) tiny = TinyVRF[Bandersnatch].prove(alpha, sk, ad) - pk_point = Bandersnatch.string_to_point(pk) - wrong_output = Bandersnatch.encode_to_curve(b"wrong-output") - assert not tiny.verify(pk, alpha, b"wrong-ad") assert not tiny.verify(pk, b"wrong-input", ad) - assert not tiny.verify_ios(pk_point, [VrfIo(Bandersnatch.encode_to_curve(alpha), wrong_output)], ad) with pytest.raises(ValueError, match="invalid Tiny VRF proof length"): TinyVRF[Bandersnatch].decode(tiny.encode()[:-1]) diff --git a/tests/test_bandersnatch_ark.py b/tests/test_bandersnatch_ark.py index d59d4bc..4c4a260 100644 --- a/tests/test_bandersnatch_ark.py +++ b/tests/test_bandersnatch_ark.py @@ -6,10 +6,10 @@ from dot_ring.curve.specs.bandersnatch import Bandersnatch from dot_ring.ring_proof.params import RingProofParams +from dot_ring.vrf.codec import scalar_len from dot_ring.vrf.ietf import TinyVRF from dot_ring.vrf.pedersen import PedersenVRF from dot_ring.vrf.ring import Ring, RingRoot, RingVRF -from dot_ring.vrf.transcript import scalar_len HERE = os.path.dirname(__file__) @@ -31,11 +31,11 @@ def test_ietf_ark_bandersnatch(): # Public Key check pk_bytes = Bandersnatch.public_key_from_secret(secret_scalar) - public_key = Bandersnatch.string_to_point(pk_bytes) + public_key = Bandersnatch.point_type.string_to_point(pk_bytes) assert public_key.point_to_string().hex() == vector["pk"] # Input Point check - input_point = Bandersnatch.encode_to_curve(vector["alpha"]) + input_point = Bandersnatch.point_type.encode_to_curve(alpha) if "h" in vector: assert input_point.point_to_string().hex() == vector["h"] @@ -77,7 +77,7 @@ def test_pedersen_ark_bandersnatch(): pk_bytes = Bandersnatch.public_key_from_secret(secret_scalar) # Input Point check - input_point = Bandersnatch.encode_to_curve(alpha) + input_point = Bandersnatch.point_type.encode_to_curve(alpha) if "h" in vector: assert input_point.point_to_string().hex() == vector["h"] diff --git a/tests/test_coverage/test_glv.py b/tests/test_coverage/test_glv.py index 1642216..8a24d99 100644 --- a/tests/test_coverage/test_glv.py +++ b/tests/test_coverage/test_glv.py @@ -75,7 +75,7 @@ def test_compute_endomorphism(self): """Test computing endomorphism.""" from dot_ring.curve.specs.bandersnatch import Bandersnatch, BandersnatchGLV - G = Bandersnatch.generator_point() + G = Bandersnatch.point_type.generator_point() phi_G = BandersnatchGLV.compute_endomorphism(G) @@ -86,7 +86,7 @@ def test_endomorphism_lambda_times_generator(self): """Test that phi(P) = lambda * P for the endomorphism.""" from dot_ring.curve.specs.bandersnatch import BANDERSNATCH_PARAMS, Bandersnatch, BandersnatchGLV - G = Bandersnatch.generator_point() + G = Bandersnatch.point_type.generator_point() phi_G = BandersnatchGLV.compute_endomorphism(G) lambda_G = G * BANDERSNATCH_PARAMS.glv_lambda @@ -102,7 +102,7 @@ def test_windowed_simultaneous_mult_basic(self): """Test windowed simultaneous multiplication.""" from dot_ring.curve.specs.bandersnatch import Bandersnatch, BandersnatchGLV - G = Bandersnatch.generator_point() + G = Bandersnatch.point_type.generator_point() phi_G = BandersnatchGLV.compute_endomorphism(G) k1 = 100 @@ -117,7 +117,7 @@ def test_windowed_mult_with_negative_k1(self): """Test windowed mult with negative k1.""" from dot_ring.curve.specs.bandersnatch import Bandersnatch, BandersnatchGLV - G = Bandersnatch.generator_point() + G = Bandersnatch.point_type.generator_point() phi_G = BandersnatchGLV.compute_endomorphism(G) k1 = -50 @@ -132,7 +132,7 @@ def test_windowed_mult_with_negative_k2(self): """Test windowed mult with negative k2.""" from dot_ring.curve.specs.bandersnatch import Bandersnatch, BandersnatchGLV - G = Bandersnatch.generator_point() + G = Bandersnatch.point_type.generator_point() phi_G = BandersnatchGLV.compute_endomorphism(G) k1 = 100 @@ -147,7 +147,7 @@ def test_windowed_mult_both_negative(self): """Test windowed mult with both k1 and k2 negative.""" from dot_ring.curve.specs.bandersnatch import Bandersnatch, BandersnatchGLV - G = Bandersnatch.generator_point() + G = Bandersnatch.point_type.generator_point() phi_G = BandersnatchGLV.compute_endomorphism(G) k1 = -30 diff --git a/tests/test_curve_ops/test_curve.py b/tests/test_curve_ops/test_curve.py index edf22ba..e9b3e98 100644 --- a/tests/test_curve_ops/test_curve.py +++ b/tests/test_curve_ops/test_curve.py @@ -58,7 +58,7 @@ def _mock_te_params(*, e2c: E2C_Variant = E2C_Variant.SSWU) -> TwistedEdwardsCur class TestCoverageCurve: def test_te_affine_double_identity(self): """Test TEAffinePoint.double with identity point.""" - identity = Ed25519_RO.identity() + identity = Ed25519_RO.point_type.identity() doubled = identity.double() assert doubled.is_identity() @@ -71,7 +71,7 @@ def test_te_affine_double_result_identity(self): def test_te_affine_double_coverage(self): """Test TEAffinePoint.double with a valid point.""" - g = Ed25519_RO.generator_point() + g = Ed25519_RO.point_type.generator_point() doubled = g.double() assert doubled == g + g @@ -80,10 +80,10 @@ def test_mg_affine_ops(self): """Test MGAffinePoint operations.""" from dot_ring.curve.specs.curve448 import Curve448_RO - g = Curve448_RO.generator_point() + g = Curve448_RO.point_type.generator_point() # Identity - identity = Curve448_RO.identity() + identity = Curve448_RO.point_type.identity() assert (g + identity) == g assert (identity + g) == g @@ -101,7 +101,7 @@ def test_mg_affine_encode_map(self): # Encode to curve msg = b"test message" - p = Curve448_RO.encode_to_curve(msg, b"DST") + p = Curve448_RO.point_type.encode_to_curve(msg, b"DST") assert p.is_on_curve() # Map to curve (if applicable directly, usually via encode) @@ -112,10 +112,10 @@ def test_sw_affine_ops(self): """Test SWAffinePoint operations.""" from dot_ring.curve.specs.p256 import P256_RO - g = P256_RO.generator_point() + g = P256_RO.point_type.generator_point() # Identity - identity = P256_RO.identity() + identity = P256_RO.point_type.identity() assert identity.is_identity() # Add with identity @@ -134,16 +134,16 @@ def test_sw_affine_from_bytes(self): """Test SWAffinePoint from_bytes (compressed/uncompressed).""" from dot_ring.curve.specs.p256 import P256_RO - g = P256_RO.generator_point() + g = P256_RO.point_type.generator_point() # Uncompressed b_uncomp = g.point_to_string(compressed=False) - p_uncomp = P256_RO.string_to_point(b_uncomp) + p_uncomp = P256_RO.point_type.string_to_point(b_uncomp) assert p_uncomp == g # Compressed b_comp = g.point_to_string(compressed=True) - p_comp = P256_RO.string_to_point(b_comp) + p_comp = P256_RO.point_type.string_to_point(b_comp) assert p_comp == g def test_sw_affine_encode(self): @@ -151,7 +151,7 @@ def test_sw_affine_encode(self): from dot_ring.curve.specs.p256 import P256_RO msg = b"test message" - p = P256_RO.encode_to_curve(msg, b"DST") + p = P256_RO.point_type.encode_to_curve(msg, b"DST") assert p.is_on_curve() def test_sw_affine_string_to_point_errors(self): @@ -160,19 +160,19 @@ def test_sw_affine_string_to_point_errors(self): # Empty string with pytest.raises(ValueError, match="Empty octet string"): - P256_RO.string_to_point(b"") + P256_RO.point_type.string_to_point(b"") # Invalid prefix with pytest.raises(ValueError, match="Invalid point encoding prefix"): - P256_RO.string_to_point(b"\x05" + b"\x00" * 32) + P256_RO.point_type.string_to_point(b"\x05" + b"\x00" * 32) # Invalid length for compressed with pytest.raises(ValueError, match="Invalid compressed point length"): - P256_RO.string_to_point(b"\x02" + b"\x00") + P256_RO.point_type.string_to_point(b"\x02" + b"\x00") # Invalid length for uncompressed with pytest.raises(ValueError, match="Invalid uncompressed point length"): - P256_RO.string_to_point(b"\x04" + b"\x00") + P256_RO.point_type.string_to_point(b"\x04" + b"\x00") def test_mg_affine_string_to_point_errors(self): """Test MGAffinePoint string_to_point error cases.""" @@ -184,7 +184,7 @@ def test_mg_affine_string_to_point_errors(self): u_bytes = b"\x00" * 56 v_bytes = b"\x01" + b"\x00" * 55 # Little endian 1 with pytest.raises(ValueError, match="Point is not on the curve"): - Curve448_RO.string_to_point(u_bytes + v_bytes) + Curve448_RO.point_type.string_to_point(u_bytes + v_bytes) def test_tonelli_shanks_coverage(self): """Test Tonelli-Shanks algorithm with a mock curve (p % 8 == 1).""" @@ -199,13 +199,17 @@ def __post_init__(self): pass curve = MockMGCurve() + mock_curve = curve + + class MockMGPoint(MGAffinePoint): + curve = mock_curve # Test _sqrt_mod_p with p=17 # Squares mod 17: 0, 1, 4, 9, 16, 8, 2, 15, 13 # 2 is a square (6^2 = 36 = 2 mod 17) # 3 is NOT a square - p = MGAffinePoint(None, None, curve) + p = MockMGPoint(None, None) # Test square root = p._sqrt_mod_p(2) @@ -229,7 +233,12 @@ def __post_init__(self): pass curve = MockMGCurve() - p = MGAffinePoint(0, 0, curve) + mock_curve = curve + + class MockMGPoint(MGAffinePoint): + curve = mock_curve + + p = MockMGPoint(0, 0) # Test point_to_string compressed error with pytest.raises(NotImplementedError, match="Compressed encoding not implemented"): @@ -237,7 +246,7 @@ def __post_init__(self): # Test _x_recover NotImplementedError with pytest.raises(NotImplementedError): - MGAffinePoint._x_recover(1, curve) + MockMGPoint._x_recover(1) def test_sw_tonelli_shanks_coverage(self): """Test SWAffinePoint.tonelli_shanks with p % 8 == 1.""" @@ -265,7 +274,7 @@ def test_sw_hybrid_format(self): """Test SWAffinePoint hybrid format (0x06/0x07).""" from dot_ring.curve.specs.p256 import P256_RO - g = P256_RO.generator_point() + g = P256_RO.point_type.generator_point() # Construct hybrid bytes manually # Prefix 0x06 if y is even, 0x07 if y is odd @@ -277,18 +286,18 @@ def test_sw_hybrid_format(self): hybrid_bytes = prefix + x_bytes + y_bytes - p = P256_RO.string_to_point(hybrid_bytes) + p = P256_RO.point_type.string_to_point(hybrid_bytes) assert p == g # Test invalid hybrid length with pytest.raises(ValueError, match="Invalid hybrid point length"): - P256_RO.string_to_point(b"\x06" + b"\x00") + P256_RO.point_type.string_to_point(b"\x06" + b"\x00") # Test invalid hybrid parity wrong_prefix = b"\x07" if y_int % 2 == 0 else b"\x06" wrong_hybrid = wrong_prefix + x_bytes + y_bytes with pytest.raises(ValueError, match="Hybrid format: y parity doesn't match prefix"): - P256_RO.string_to_point(wrong_hybrid) + P256_RO.point_type.string_to_point(wrong_hybrid) def test_curve_point_base_coverage(self): """Test base CurvePoint methods.""" @@ -302,7 +311,12 @@ def __init__(self): def __post_init__(self): pass + c = MockCurve() + mock_curve = c + class MockPoint(CurvePoint): + curve = mock_curve + def is_on_curve(self): return True @@ -313,11 +327,10 @@ def _validate_coordinates(self): return True @classmethod - def identity(cls, curve): - return cls(None, None, curve) + def identity(cls): + return cls(None, None) - c = MockCurve() - p = MockPoint(0, 0, curve=c) + p = MockPoint(0, 0) # Test NotImplementedError with pytest.raises(NotImplementedError): @@ -329,14 +342,14 @@ def identity(cls, curve): # Test msm errors with pytest.raises(ValueError, match="Points and scalars must have same length"): - MockPoint.msm([p], [1, 2], c) + MockPoint.msm([p], [1, 2]) # Test msm empty # Should return identity - res = MockPoint.msm([], [], c) + res = MockPoint.msm([], []) assert res.x is None and res.y is None - p_hash = MockPoint(1, 2, curve=c) + p_hash = MockPoint(1, 2) h = hash(p_hash) assert isinstance(h, int) @@ -344,7 +357,7 @@ def test_te_affine_errors(self): """Test TEAffinePoint error cases.""" from dot_ring.curve.specs.ed25519 import Ed25519_RO - g = Ed25519_RO.generator_point() + g = Ed25519_RO.point_type.generator_point() # __add__ invalid type with pytest.raises(TypeError, match="Can only add TEAffinePoints"): @@ -366,20 +379,24 @@ def __init__(self): super().__init__(params=_mock_te_params(), e2c_variant=E2C_Variant.SSWU) curve = MockTECurve() + mock_curve = curve + + class MockTEPoint(TEAffinePoint): + curve = mock_curve # Unsupported TE hash-to-curve variants fail at dispatch. with pytest.raises(ValueError, match="Unexpected E2C Variant"): - TEAffinePoint.encode_to_curve(b"test", curve=curve) + MockTEPoint.encode_to_curve(b"test") # Test point_to_string uncompressed # MockTECurve uses uncompressed encoding. - p = TEAffinePoint(0, 1, curve) # Identity + p = MockTEPoint(0, 1) # Identity # Identity serialization should succeed for TE (0, 1) s = p.point_to_string() assert len(s) == 2 # 1 byte x, 1 byte y (p=17 -> 5 bits -> 1 byte) # Valid point serialization (uncompressed) - p_valid = TEAffinePoint(0, 16, curve) # 16 = -1 mod 17 + p_valid = MockTEPoint(0, 16) # 16 = -1 mod 17 s = p_valid.point_to_string() assert len(s) == 2 # 1 byte x, 1 byte y @@ -387,7 +404,7 @@ def test_curve_point_hash_fp2(self): """Test CurvePoint.__hash__ with explicit Fp2 coordinates.""" from dot_ring.curve.specs.bls12_381_G2 import BLS12_381_G2_RO - point = BLS12_381_G2_RO.generator_point() + point = BLS12_381_G2_RO.point_type.generator_point() assert isinstance(point.x, Fp2) assert isinstance(point.y, Fp2) expected = (point.x.re + point.x.im + point.y.re + point.y.im) % point.curve.params.subgroup_order diff --git a/tests/test_curve_ops/test_curve_ops.py b/tests/test_curve_ops/test_curve_ops.py index 5ac68ef..c306fcc 100644 --- a/tests/test_curve_ops/test_curve_ops.py +++ b/tests/test_curve_ops/test_curve_ops.py @@ -43,7 +43,7 @@ def test_curve_property_based(curve_variant): - Scalar wrap-around: (order * P) == identity - Negation: P + (-P) == identity """ - Generator = curve_variant.generator_point() + Generator = curve_variant.point_type.generator_point() order = curve_variant.curve.params.subgroup_order # Random scalars within valid range @@ -98,8 +98,8 @@ def test_curve_sanity_operations(): ] for curve_variant in curve_data: - Generator = curve_variant.generator_point() - Identity = curve_variant.identity() + Generator = curve_variant.point_type.generator_point() + Identity = curve_variant.point_type.identity() Order = curve_variant.curve.params.subgroup_order # Addition with identity diff --git a/tests/test_curve_ops/test_curves_s2p_p2s.py b/tests/test_curve_ops/test_curves_s2p_p2s.py index 00090b7..0a37f09 100644 --- a/tests/test_curve_ops/test_curves_s2p_p2s.py +++ b/tests/test_curve_ops/test_curves_s2p_p2s.py @@ -5,21 +5,21 @@ def test_s2p_p2s_correctness(): # test for Bandersnatch curve - point = Bandersnatch.generator_point() + point = Bandersnatch.point_type.generator_point() S_repr = point.point_to_string() - assert point == Bandersnatch.string_to_point(S_repr), "Invalid Conversion" + assert point == Bandersnatch.point_type.string_to_point(S_repr), "Invalid Conversion" # Test for P256 curve - point = P256_RO.generator_point() + point = P256_RO.point_type.generator_point() S_repr = point.point_to_string() - assert point == P256_RO.string_to_point(S_repr), "Invalid Conversion" + assert point == P256_RO.point_type.string_to_point(S_repr), "Invalid Conversion" # Test for P256 Uncompressed - point = P256_RO.generator_point() + point = P256_RO.point_type.generator_point() S_repr = point.point_to_string(compressed=True) - assert point == P256_RO.string_to_point(S_repr), "Invalid Conversion" + assert point == P256_RO.point_type.string_to_point(S_repr), "Invalid Conversion" # Test for Curve25519 curve - point = Curve25519_RO.generator_point() + point = Curve25519_RO.point_type.generator_point() S_repr = point.point_to_string() - assert point == Curve25519_RO.string_to_point(S_repr), "Invalid Conversion" + assert point == Curve25519_RO.point_type.string_to_point(S_repr), "Invalid Conversion" diff --git a/tests/test_curve_ops/test_gaps.py b/tests/test_curve_ops/test_gaps.py index a36eff8..3d49538 100644 --- a/tests/test_curve_ops/test_gaps.py +++ b/tests/test_curve_ops/test_gaps.py @@ -3,8 +3,8 @@ from dot_ring import Bandersnatch, PedersenVRF, TinyVRF from dot_ring.curve.specs.ed448 import Ed448_RO from dot_ring.ring_proof.params import RingProofParams +from dot_ring.vrf.codec import point_len, scalar_len from dot_ring.vrf.ring import Ring, RingRoot -from dot_ring.vrf.transcript import point_len, scalar_len class TestCoverageGaps: @@ -59,7 +59,7 @@ def test_ecvrf_decode_proof_invalid_s(self): invalid_proof = gamma_bytes + c_bytes + invalid_s_bytes - with pytest.raises(ValueError, match="Response scalar S is not less than the curve order"): + with pytest.raises(ValueError, match="scalar is not canonical"): TinyVRF[Bandersnatch].ecvrf_decode_proof(invalid_proof) def test_ecvrf_proof_to_hash_string_input(self): @@ -109,7 +109,7 @@ def test_ietf_decode_invalid_s(self): invalid_proof = gamma_bytes + c_bytes + invalid_s_bytes - with pytest.raises(ValueError, match="Response scalar s is not less than the curve order"): + with pytest.raises(ValueError, match="scalar is not canonical"): TinyVRF[Bandersnatch].decode(invalid_proof) def test_ietf_verify_invalid_public_key(self): diff --git a/tests/test_curve_ops/test_glv.py b/tests/test_curve_ops/test_glv.py index d4f2e46..bbd935a 100644 --- a/tests/test_curve_ops/test_glv.py +++ b/tests/test_curve_ops/test_glv.py @@ -9,8 +9,8 @@ def test_glv_identity_handling(self): """Test GLV methods with identity points.""" glv = GLV(lambda_param=1, constant_b=1, constant_c=1) - g = Ed25519_RO.generator_point() - identity = Ed25519_RO.identity() + g = Ed25519_RO.point_type.generator_point() + identity = Ed25519_RO.point_type.identity() # Test windowed_simultaneous_mult with identity # P1 identity diff --git a/tests/test_curve_ops/test_mg_curve.py b/tests/test_curve_ops/test_mg_curve.py index 99d2c60..5a267f5 100644 --- a/tests/test_curve_ops/test_mg_curve.py +++ b/tests/test_curve_ops/test_mg_curve.py @@ -51,15 +51,15 @@ def test_mg_curve_methods(): invalid_point = (1, 0) assert not curve.is_on_curve(invalid_point) - # Test point_at_infinity - inf = curve.point_at_infinity() + # Point construction is suite-bound; MGCurve itself does not own a point class. + inf = Curve25519_RO.point_type.identity() assert inf.is_identity() # Test validate_point assert curve.validate_point(inf) # Test validate_point with valid point - gen_point = Curve25519_RO.generator_point() + gen_point = Curve25519_RO.point_type.generator_point() assert curve.validate_point(gen_point) # Test validate_point with invalid point object (mock) @@ -75,7 +75,7 @@ def is_identity(self): def test_mg_curve_random_point(): curve = Curve25519_RO.curve - point = curve.random_point() + point = Curve25519_RO.point_type.encode_to_curve(b"random point") assert curve.is_on_curve((point.x, point.y)) assert curve.validate_point(point) diff --git a/tests/test_dot_ring_vectors.py b/tests/test_dot_ring_vectors.py index b4826c6..ad461b2 100644 --- a/tests/test_dot_ring_vectors.py +++ b/tests/test_dot_ring_vectors.py @@ -26,7 +26,7 @@ def test_dot_ring_tiny_vectors(suite: Suite) -> None: proof = TinyVRF[suite.curve].decode(proof_bytes) assert suite.curve.public_key_from_secret(bytes.fromhex(vector["sk"])).hex() == vector["pk"] - assert suite.curve.encode_to_curve(alpha).point_to_string().hex() == vector["h"] + assert suite.curve.point_type.encode_to_curve(alpha).point_to_string().hex() == vector["h"] assert proof.verify(bytes.fromhex(vector["pk"]), alpha, ad) assert TinyVRF[suite.curve].proof_to_hash(proof.output_point).hex() == vector["beta"] diff --git a/tests/test_h2c_suites/test_bls12_381_G2_ssw_nu.py b/tests/test_h2c_suites/test_bls12_381_G2_ssw_nu.py index f03e16f..bc197a2 100644 --- a/tests/test_h2c_suites/test_bls12_381_G2_ssw_nu.py +++ b/tests/test_h2c_suites/test_bls12_381_G2_ssw_nu.py @@ -32,7 +32,7 @@ def test_sswu_hash2_curve(self): p = BLS12_381_G2_NU.curve.params.field_modulus # Encode message to curve - computed_P = BLS12_381_G2_NU.encode_to_curve(msg.encode("utf-8"), b"") + computed_P = BLS12_381_G2_NU.point_type.encode_to_curve(msg.encode("utf-8"), b"") # Create sample.py P point expected_P_x = self.hex_to_fp2(expected_P["x"], p) diff --git a/tests/test_h2c_suites/test_bls12_381_G2_ssw_ro.py b/tests/test_h2c_suites/test_bls12_381_G2_ssw_ro.py index 10fb9e5..80a22a8 100644 --- a/tests/test_h2c_suites/test_bls12_381_G2_ssw_ro.py +++ b/tests/test_h2c_suites/test_bls12_381_G2_ssw_ro.py @@ -32,7 +32,7 @@ def test_sswu_hash2_curve(self): p = BLS12_381_G2_RO.curve.params.field_modulus # Encode message to curve - computed_P = BLS12_381_G2_RO.encode_to_curve(msg.encode("utf-8"), b"") + computed_P = BLS12_381_G2_RO.point_type.encode_to_curve(msg.encode("utf-8"), b"") # Create sample.py P point expected_P_x = self.hex_to_fp2(expected_P["x"], p) diff --git a/tests/test_h2c_suites/test_e2c_bandersnatch.py b/tests/test_h2c_suites/test_e2c_bandersnatch.py index 798eb3c..f6ef283 100644 --- a/tests/test_h2c_suites/test_e2c_bandersnatch.py +++ b/tests/test_h2c_suites/test_e2c_bandersnatch.py @@ -16,8 +16,8 @@ def test_m2c(): data = bytes("foo", "utf-8") u = Bandersnatch_TE_Curve.hash_to_field(data, 2) - p0 = BandersnatchPoint.map_to_curve(u[0], Bandersnatch_TE_Curve) - p1 = BandersnatchPoint.map_to_curve(u[1], Bandersnatch_TE_Curve) + p0 = BandersnatchPoint.map_to_curve(u[0]) + p1 = BandersnatchPoint.map_to_curve(u[1]) assert p0.x == 8864805491392651408849860969071502422330330403516577719645408615048305804698 assert p0.y == 3141991639291324936022779954882288522159181883763336030616570191472121730763 @@ -28,7 +28,7 @@ def test_m2c(): def test_e2c(): data = bytes("foo", "utf-8") - u = Bandersnatch.encode_to_curve(data) + u = Bandersnatch.point_type.encode_to_curve(data) assert u.x == 41706851287321768980670436615954402659160947743433584884323702829779219804533 assert u.y == 45261115535002764022712885934321790255618221679857277845409327068867281988279 diff --git a/tests/test_h2c_suites/test_h2c_nu.py b/tests/test_h2c_suites/test_h2c_nu.py index 14e0218..3535038 100644 --- a/tests/test_h2c_suites/test_h2c_nu.py +++ b/tests/test_h2c_suites/test_h2c_nu.py @@ -40,7 +40,7 @@ def test_h2c_nu(curve_variant, json_file, byte_size): for i, t in enumerate(vectors, start=1): msg_bytes = t["msg"].encode("utf-8") - point = curve_variant.encode_to_curve(msg_bytes, b"") + point = curve_variant.point_type.encode_to_curve(msg_bytes, b"") Px_bytes = point.x.to_bytes(byte_size, "big").hex() Py_bytes = point.y.to_bytes(byte_size, "big").hex() diff --git a/tests/test_h2c_suites/test_h2c_ro.py b/tests/test_h2c_suites/test_h2c_ro.py index e13b6ff..a833989 100644 --- a/tests/test_h2c_suites/test_h2c_ro.py +++ b/tests/test_h2c_suites/test_h2c_ro.py @@ -40,7 +40,7 @@ def test_h2c_ro(curve_variant, json_file, byte_size): for i, t in enumerate(vectors, start=1): msg_bytes = t["msg"].encode("utf-8") - point = curve_variant.encode_to_curve(msg_bytes, b"") + point = curve_variant.point_type.encode_to_curve(msg_bytes, b"") Px_bytes = point.x.to_bytes(byte_size, "big").hex() Py_bytes = point.y.to_bytes(byte_size, "big").hex() diff --git a/tests/test_ietf/test_ietf_ark.py b/tests/test_ietf/test_ietf_ark.py index 95e5d1e..9c44c2a 100644 --- a/tests/test_ietf/test_ietf_ark.py +++ b/tests/test_ietf/test_ietf_ark.py @@ -49,11 +49,11 @@ def test_ietf_ark(curve_variant, file_prefix, subdir, gamma_len): # Public Key check pk_bytes = curve_variant.public_key_from_secret(secret_scalar) - public_key = curve_variant.string_to_point(pk_bytes) + public_key = curve_variant.point_type.string_to_point(pk_bytes) assert public_key.point_to_string().hex() == vector["pk"] # Input Point check - input_point = curve_variant.encode_to_curve(alpha, salt) + input_point = curve_variant.point_type.encode_to_curve(alpha, salt) if "h" in vector: assert input_point.point_to_string().hex() == vector["h"] diff --git a/tests/test_pedersen/test_pedersen_ark.py b/tests/test_pedersen/test_pedersen_ark.py index 6ad5c2e..65de4ee 100644 --- a/tests/test_pedersen/test_pedersen_ark.py +++ b/tests/test_pedersen/test_pedersen_ark.py @@ -9,8 +9,8 @@ from dot_ring.curve.specs.ed25519 import Ed25519_TAI from dot_ring.curve.specs.jubjub import JubJub from dot_ring.curve.specs.p256 import P256_TAI +from dot_ring.vrf.codec import scalar_len from dot_ring.vrf.pedersen import PedersenVRF -from dot_ring.vrf.transcript import scalar_len HERE = os.path.dirname(__file__) @@ -49,7 +49,7 @@ def test_pedersen_ietf(curve_variant, file_prefix, subdir): pk_bytes = curve_variant.public_key_from_secret(secret_scalar) # Input Point check - input_point = curve_variant.encode_to_curve(alpha) + input_point = curve_variant.point_type.encode_to_curve(alpha) if "h" in vector: assert input_point.point_to_string().hex() == vector["h"] diff --git a/tests/test_pedersen/test_pedersen_base.py b/tests/test_pedersen/test_pedersen_base.py index c1ef9ab..901e74f 100644 --- a/tests/test_pedersen/test_pedersen_base.py +++ b/tests/test_pedersen/test_pedersen_base.py @@ -51,7 +51,7 @@ def test_pedersen_base(curve_variant, file_prefix, slice_end): alpha = bytes.fromhex(vector["alpha"]) additional_data = bytes.fromhex(vector["ad"]) - curve_variant.encode_to_curve(alpha) + curve_variant.point_type.encode_to_curve(alpha) proof = PedersenVRF[curve_variant].prove(alpha, secret_scalar, additional_data) proof_bytes = proof.encode() diff --git a/tests/test_ring_vrf/test_audit_regressions.py b/tests/test_ring_vrf/test_audit_regressions.py index c4fb761..012f48e 100644 --- a/tests/test_ring_vrf/test_audit_regressions.py +++ b/tests/test_ring_vrf/test_audit_regressions.py @@ -2,9 +2,9 @@ from dot_ring import Bandersnatch, RingVRF from dot_ring.ring_proof.params import RingProofParams +from dot_ring.vrf.codec import point_len from dot_ring.vrf.pedersen import PedersenVRF -from dot_ring.vrf.ring import Ring, RingContext, RingRoot -from dot_ring.vrf.transcript import point_len +from dot_ring.vrf.ring import Ring, RingRoot def _keys(count: int) -> list[bytes]: @@ -12,7 +12,7 @@ def _keys(count: int) -> list[bytes]: def _point_tuple(key: bytes) -> tuple[int, int]: - point = Bandersnatch.string_to_point(key) + point = Bandersnatch.point_type.string_to_point(key) assert not isinstance(point, str) return (point.x, point.y) @@ -106,22 +106,6 @@ def test_prove_rejects_producer_key_that_does_not_match_secret(): RingVRF[Bandersnatch].prove(b"audit-input", b"audit-ad", sk1, pk2, ring, ring_root) -def test_ring_root_builder_caches_root_when_full(): - keys = _keys(8) - params = RingProofParams(test_vectors=True, max_ring_size=8) - context = RingContext(params) - builder = context.ring_root_builder() - - builder.append(keys) - built_root = builder.finalize() - direct_root = context.ring_root(keys) - - assert built_root.encode() == direct_root.encode() - assert builder.finalize() is built_root - with pytest.raises(ValueError, match="too many keys"): - builder.push(keys[0]) - - def test_unsupported_params_fail_at_construction(): with pytest.raises(ValueError, match="padding_rows must be 4"): RingProofParams(padding_rows=5, max_ring_size=1) diff --git a/tests/test_vectors.py b/tests/test_vectors.py index 6e23293..a5d3f08 100644 --- a/tests/test_vectors.py +++ b/tests/test_vectors.py @@ -7,8 +7,8 @@ from dot_ring import P256, Bandersnatch, Ed25519, PedersenVRF, RingVRF, TinyVRF from dot_ring.ring_proof.params import RingProofParams +from dot_ring.vrf.codec import point_len, scalar_len from dot_ring.vrf.ring import Ring, RingRoot -from dot_ring.vrf.transcript import point_len, scalar_len # Alias Secp256r1 = P256 From 61fe55f8f064ee98896c0b8543472336c1fb914e Mon Sep 17 00:00:00 2001 From: Prasad Kumkar Date: Tue, 23 Jun 2026 15:52:13 +0530 Subject: [PATCH 4/4] feat: caching ring objects, optimised&clearer point validation handling, slight refactor --- dot_ring/curve/curve.py | 16 +- dot_ring/curve/montgomery/mg_affine_point.py | 34 +-- dot_ring/curve/point.py | 59 ++--- dot_ring/curve/specs/bandersnatch_sw.py | 9 +- dot_ring/ring_proof/columns/columns.py | 18 +- dot_ring/ring_proof/proof_builder.py | 32 ++- dot_ring/ring_proof/proof_payload.py | 22 +- dot_ring/ring_proof/verify.py | 220 ++++++------------- dot_ring/vrf/codec.py | 28 +-- dot_ring/vrf/ietf/thin.py | 22 +- dot_ring/vrf/ietf/tiny.py | 37 +--- dot_ring/vrf/pedersen/vrf.py | 202 ++++------------- dot_ring/vrf/ring/members.py | 60 ++--- dot_ring/vrf/ring/root.py | 177 ++++++++------- dot_ring/vrf/ring/vrf.py | 71 +++--- tests/benchmark/bench_ring_proof.py | 45 ++-- tests/test_bandersnatch_ark.py | 6 +- tests/test_curve_ops/test_gaps.py | 91 +------- tests/test_ietf/test_ietf_ark.py | 2 +- tests/test_pedersen/test_pedersen_ark.py | 4 +- 20 files changed, 404 insertions(+), 751 deletions(-) diff --git a/dot_ring/curve/curve.py b/dot_ring/curve/curve.py index ebc6f13..8ad5c99 100644 --- a/dot_ring/curve/curve.py +++ b/dot_ring/curve/curve.py @@ -53,6 +53,19 @@ def encoding_endian(self) -> Literal["little", "big"]: return "little" return self.params.encoding.endian + def valid_point(self, point: CurvePoint) -> bool: + try: + if point.is_identity() or not point.is_on_curve(): + return False + cofactor = self.params.cofactor + if cofactor == 1: + return True + cofactor_inv = _powmod(cofactor, -1, self.params.subgroup_order) + cleared = point * cofactor + return not cleared.is_identity() and cleared * cofactor_inv == point + except (AttributeError, TypeError, ValueError): + return False + def _validate(self): """ Validate that the curve parameters are correct. @@ -372,7 +385,8 @@ def public_key_from_secret(self, secret_key: bytes) -> bytes: if not isinstance(secret_key, bytes | bytearray): raise TypeError("secret_key must be bytes") secret_scalar = int.from_bytes(secret_key, "little") - return (self.point_type.generator_point() * secret_scalar).point_to_string() + point = self.point_type.generator_point() * secret_scalar + return point.point_to_string() def secret_from_seed(self, seed: bytes) -> tuple[bytes, bytes]: if not isinstance(seed, bytes | bytearray): diff --git a/dot_ring/curve/montgomery/mg_affine_point.py b/dot_ring/curve/montgomery/mg_affine_point.py index 24ddc4d..5d1b89a 100644 --- a/dot_ring/curve/montgomery/mg_affine_point.py +++ b/dot_ring/curve/montgomery/mg_affine_point.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Literal, TypeVar, cast +from typing import TypeVar, cast from dot_ring.curve.e2c import E2C_Variant @@ -67,22 +67,22 @@ def __add__(self, other: MGAffinePoint[C]) -> MGAffinePoint[C]: # Should be handled by is_identity checks above, but for mypy: return self.__class__(None, None) - x1, y1 = cast(int, self.x) % p, cast(int, self.y) % p - x2, y2 = cast(int, other.x) % p, cast(int, other.y) % p + x1, y1 = self.x % p, self.y % p + x2, y2 = other.x % p, other.y % p # Doubling if x1 == x2 and y1 == y2: # if y == 0 then slope denominator = 0 => result is identity if y1 % p == 0: return self.__class__(None, None) - numerator = (3 * cast(int, x1) * cast(int, x1) + 2 * cast(int, A) * cast(int, x1) + 1) % p - denominator = (2 * cast(int, B) * cast(int, y1)) % p + numerator = (3 * x1 * x1 + 2 * A * x1 + 1) % p + denominator = (2 * B * y1) % p # Check if denominator is zero before computing inverse if denominator == 0: return self.__class__(None, None) lam = (numerator * pow(denominator, -1, p)) % p - x3 = (cast(int, B) * (lam * lam % p) - cast(int, A) - 2 * cast(int, x1)) % p - y3 = (lam * (cast(int, x1) - x3) - cast(int, y1)) % p + x3 = (B * (lam * lam % p) - A - 2 * x1) % p + y3 = (lam * (x1 - x3) - y1) % p return self.__class__(x3, y3) # Addition for distinct points @@ -90,16 +90,16 @@ def __add__(self, other: MGAffinePoint[C]) -> MGAffinePoint[C]: # vertical line -> identity (x1 == x2 but y1 != y2) return self.__class__(None, None) - numerator = (cast(int, y2) - cast(int, y1)) % p - denominator = (cast(int, x2) - cast(int, x1)) % p + numerator = (y2 - y1) % p + denominator = (x2 - x1) % p # Check if denominator is zero (shouldn't happen since x1 != x2) if denominator == 0: raise ValueError("Unexpected zero denominator in point addition") lam = (numerator * pow(denominator, -1, p)) % p # Corrected formula for x3 in point addition - x3 = (cast(int, B) * lam * lam - cast(int, A) - cast(int, x1) - cast(int, x2)) % p + x3 = (B * lam * lam - A - x1 - x2) % p # Corrected formula for y3 - y3 = (lam * (cast(int, x1) - x3) - cast(int, y1)) % p + y3 = (lam * (x1 - x3) - y1) % p return self.__class__(x3, y3) def __neg__(self) -> MGAffinePoint[C]: @@ -107,8 +107,8 @@ def __neg__(self) -> MGAffinePoint[C]: if self.is_identity() or self.x is None or self.y is None: return self.__class__(None, None) return self.__class__( - cast(int, self.x) % self.curve.params.field_modulus, - (-cast(int, self.y)) % self.curve.params.field_modulus, + self.x % self.curve.params.field_modulus, + (-self.y) % self.curve.params.field_modulus, ) def __sub__(self, other: MGAffinePoint[C]) -> MGAffinePoint[C]: @@ -355,8 +355,8 @@ def point_to_string(self) -> bytes: # Encode u and v coordinates as little-endian bytes if self.x is None or self.y is None: raise ValueError("Cannot serialize identity point") - x_bytes = int(cast(int, self.x)).to_bytes(field_byte_len, cast(Literal["little", "big"], self.curve.encoding_endian())) - y_bytes = int(cast(int, self.y)).to_bytes(field_byte_len, cast(Literal["little", "big"], self.curve.encoding_endian())) + x_bytes = self.x.to_bytes(field_byte_len, self.curve.params.encoding.endian) + y_bytes = self.y.to_bytes(field_byte_len, self.curve.params.encoding.endian) return x_bytes + y_bytes else: raise NotImplementedError("Compressed encoding not implemented") @@ -373,8 +373,8 @@ def string_to_point(cls, data: str | bytes) -> MGAffinePoint: u_bytes = data[:byte_length] v_bytes = data[byte_length:] - u = int.from_bytes(u_bytes, cls.curve.encoding_endian()) - v = int.from_bytes(v_bytes, cls.curve.encoding_endian()) + u = int.from_bytes(u_bytes, cls.curve.params.encoding.endian) + v = int.from_bytes(v_bytes, cls.curve.params.encoding.endian) # Create the point point = cls(u, v) diff --git a/dot_ring/curve/point.py b/dot_ring/curve/point.py index 69af492..e12aff4 100644 --- a/dot_ring/curve/point.py +++ b/dot_ring/curve/point.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import abstractmethod -from typing import TYPE_CHECKING, Generic, Self, TypeVar, cast +from typing import TYPE_CHECKING, Generic, Self, TypeVar from dot_ring.curve.e2c import E2C_Variant from dot_ring.curve.fp2 import Fp2 @@ -81,7 +81,7 @@ def _validate_coordinates(self) -> bool: if self.is_identity(): return True field_modulus = self.curve.params.field_modulus - return 0 <= cast(int, self.x) < field_modulus and 0 <= cast(int, self.y) < field_modulus + return 0 <= self.x < field_modulus and 0 <= self.y < field_modulus @classmethod def msm(cls, points: list[Self], scalars: list[int]) -> Self: @@ -166,16 +166,16 @@ def point_to_string(self) -> bytes: p = self.curve.params.field_modulus n_bytes = (p.bit_length() + 7) // 8 - y_bytes = bytearray(cast(int, self.y).to_bytes(n_bytes, self.curve.encoding_endian())) + y_bytes = bytearray(self.y.to_bytes(n_bytes, self.curve.params.encoding.endian)) # Compute x sign bit - x_sign_bit = 1 if cast(int, self.x) > (-cast(int, self.x) % p) else 0 + x_sign_bit = 1 if self.x > -self.x % p else 0 y_bytes[-1] |= x_sign_bit << 7 encoded = bytes(y_bytes) return encoded @classmethod - def string_to_point(cls, octet_string: str | bytes) -> Self: + def string_to_point(cls, octet_string: bytes) -> Self: """ Convert compressed octet string back to point. Args: @@ -188,20 +188,17 @@ def string_to_point(cls, octet_string: str | bytes) -> Self: ValueError: If encoding is invalid. """ - curve = cls.curve - if curve.params.encoding.uncompressed: - return cls.uncompressed_s2p(octet_string) - - if isinstance(octet_string, str): - octet_string = bytes.fromhex(octet_string) if not octet_string: raise ValueError("Empty octet string") + if cls.curve.params.encoding.uncompressed: + return cls.uncompressed_s2p(octet_string) + x_sign_bit = (octet_string[-1] >> 7) & 1 y_bytes = bytearray(octet_string) y_bytes[-1] &= 0x7F - y = int.from_bytes(y_bytes, curve.encoding_endian()) - if y >= curve.params.field_modulus: + y = int.from_bytes(y_bytes, cls.curve.params.encoding.endian) + if y >= cls.curve.params.field_modulus: raise ValueError("Invalid point encoding") x_candidates = cls._x_recover(y) @@ -209,7 +206,7 @@ def string_to_point(cls, octet_string: str | bytes) -> Self: raise ValueError("Invalid point encoding") if isinstance(x_candidates, int): - x, neg_x = x_candidates, (-x_candidates) % curve.params.field_modulus + x, neg_x = x_candidates, (-x_candidates) % cls.curve.params.field_modulus else: x, neg_x = x_candidates @@ -220,28 +217,20 @@ def uncompressed_p2s(self) -> bytes: p = self.curve.params.field_modulus byte_length = (p.bit_length() + 7) // 8 # Encode u and v coordinates as little-endian bytes - x_bytes = cast(int, self.x).to_bytes(byte_length, self.curve.encoding_endian()) - y_bytes = cast(int, self.y).to_bytes(byte_length, self.curve.encoding_endian()) + x_bytes = self.x.to_bytes(byte_length, self.curve.params.encoding.endian) + y_bytes = self.y.to_bytes(byte_length, self.curve.params.encoding.endian) return x_bytes + y_bytes @classmethod - def uncompressed_s2p(cls, octet_string: str | bytes) -> Self: + def uncompressed_s2p(cls, octet_string: bytes) -> Self: curve = cls.curve - if isinstance(octet_string, str): - octet_string = bytes.fromhex(octet_string) p = curve.params.field_modulus byte_length = (p.bit_length() + 7) // 8 # Split into u and v coordinates - x_bytes = octet_string[:byte_length] - y_bytes = octet_string[byte_length:] - x = int.from_bytes(x_bytes, curve.encoding_endian()) - y = int.from_bytes(y_bytes, curve.encoding_endian()) + x = int.from_bytes(octet_string[:byte_length], curve.params.encoding.endian) + y = int.from_bytes(octet_string[byte_length:], curve.params.encoding.endian) # Create the point - point = cls(x % curve.params.field_modulus, y % curve.params.field_modulus) - # Verify the point is on the curve - if not point.is_on_curve(): - raise ValueError("Point is not on the curve") - return point + return cls(x, y) @classmethod def _x_recover(cls, y: int) -> int | tuple[int, int] | None: @@ -309,19 +298,11 @@ def encode_to_curve( def __hash__(self) -> int: if self.x is None or self.y is None: return 0 - - x_val = 0 - if isinstance(self.x, int): + if isinstance(self.x, int) and isinstance(self.y, int): x_val = self.x - elif isinstance(self.x, Fp2): - x_val = self.x.re + self.x.im - else: - raise TypeError(f"Unsupported point coordinate type: {type(self.x).__name__}") - - y_val = 0 - if isinstance(self.y, int): y_val = self.y - elif isinstance(self.y, Fp2): + elif isinstance(self.x, Fp2) and isinstance(self.y, Fp2): + x_val = self.x.re + self.x.im y_val = self.y.re + self.y.im else: raise TypeError(f"Unsupported point coordinate type: {type(self.y).__name__}") diff --git a/dot_ring/curve/specs/bandersnatch_sw.py b/dot_ring/curve/specs/bandersnatch_sw.py index e902a87..bd474d5 100644 --- a/dot_ring/curve/specs/bandersnatch_sw.py +++ b/dot_ring/curve/specs/bandersnatch_sw.py @@ -1,7 +1,7 @@ from __future__ import annotations import hashlib -from typing import Literal, Self, cast +from typing import Self from dot_ring.curve.curve import CurveVariant from dot_ring.curve.e2c import E2C_Variant @@ -74,9 +74,8 @@ def point_to_string(self, compressed: bool = False) -> bytes: if self.x is None or self.y is None: raise ValueError("Cannot serialize identity point") - y_int = cast(int, self.y) - flag = 0 if y_int <= (-y_int % p) else 1 << 7 - x_bytes = int(cast(int, self.x)).to_bytes((field_bit_len + 7) // 8, cast(Literal["little", "big"], self.curve.encoding_endian())) + flag = 0 if self.y <= (-self.y % p) else 1 << 7 + x_bytes = self.x.to_bytes((field_bit_len + 7) // 8, self.curve.params.encoding.endian) result = bytearray(output_byte_len) result[: len(x_bytes)] = x_bytes @@ -110,7 +109,7 @@ def string_to_point(cls, data: str | bytes) -> Self: raise ValueError("Empty octet string") x_bytes = data[:-1] - x = int.from_bytes(x_bytes, curve.encoding_endian()) + x = int.from_bytes(x_bytes, curve.params.encoding.endian) y_candidates = cls._y_recover(x) if not y_candidates: raise ValueError("Invalid point: no y-coordinate found for x") diff --git a/dot_ring/ring_proof/columns/columns.py b/dot_ring/ring_proof/columns/columns.py index e7bf9bc..28415cf 100644 --- a/dot_ring/ring_proof/columns/columns.py +++ b/dot_ring/ring_proof/columns/columns.py @@ -23,7 +23,7 @@ class Column: name: str evals: list[int] coeffs: list[int] | None = None - commitment: G1Commitment | None = None + _commitment: G1Commitment | None = None size: int = DEFAULT_DOMAIN_SIZE def interpolate( @@ -55,14 +55,14 @@ def interpolate( def commit(self, pcs: type[PCS] = KZG) -> None: if self.coeffs is None: raise ValueError("call interpolate() first") - if self.commitment is None: - self.commitment = pcs.commit(self.coeffs) - - -def require_commitment(column: Column) -> G1Commitment: - if column.commitment is None: - raise ValueError(f"{column.name} commitment is missing") - return column.commitment + if self._commitment is None: + self._commitment = pcs.commit(self.coeffs) + + @property + def commitment(self) -> G1Commitment | None: + if self._commitment is None: + raise ValueError(f"{self.name} commitment is not set") + return self._commitment @dataclass(slots=True) diff --git a/dot_ring/ring_proof/proof_builder.py b/dot_ring/ring_proof/proof_builder.py index 5808832..b2c71a3 100644 --- a/dot_ring/ring_proof/proof_builder.py +++ b/dot_ring/ring_proof/proof_builder.py @@ -7,7 +7,7 @@ from dot_ring.curve.curve import CurveVariant from dot_ring.curve.point import CurvePoint -from dot_ring.ring_proof.columns.columns import Column, WitnessColumnBuilder, require_commitment +from dot_ring.ring_proof.columns.columns import Column, WitnessColumnBuilder from dot_ring.ring_proof.constraints.constraints import RingConstraintBuilder from dot_ring.ring_proof.polynomial.fft import inverse_fft from dot_ring.ring_proof.polynomial.ops import poly_add, poly_divide_by_vanishing, poly_evaluate_single, poly_multiply, poly_scalar_mul @@ -38,12 +38,20 @@ def __init__( def build(self) -> RingProofPayload: """Spec sections 3.1-3.3 in order: witness, constraints, quotient, linearization, openings.""" params = self.ring.params - if self.ring_root is None: - from dot_ring.vrf.ring.root import RingRoot + from dot_ring.vrf.ring.root import RingRoot - ring_root = RingRoot.from_ring(self.ring, params) - else: - ring_root = self.ring_root + ring_root = self.ring_root + if ( + ring_root is None + or ring_root.px.coeffs is None + or ring_root.py.coeffs is None + or ring_root.s.coeffs is None + or len(ring_root.s.evals) < params.domain_size + ): + computed_root = RingRoot.from_ring(self.ring, params) + if ring_root is not None and computed_root.encode() != ring_root.encode(): + raise ValueError("ring_root does not match ring") + ring_root = computed_root producer_index = self.ring.index_of(self.producer_key) # Section 3.1: witness bit column and accumulators for R = PK_k + bB. @@ -79,7 +87,7 @@ def build(self) -> RingProofPayload: # Section 3.3.2: quotient commitment. q_poly = poly_divide_by_vanishing(c_agg, params.domain_size) c_q = params.pcs.commit(q_poly) - c_q_column = Column(name="C_q", evals=[], commitment=c_q) + c_q_column = Column(name="C_q", evals=[], _commitment=c_q) fixed_columns = (ring_root.px, ring_root.py, ring_root.s) @@ -142,12 +150,12 @@ def _phase1_transcript( params = self.ring.params c_b, c_accx, c_accy, c_accip = witness_columns witness_commitments = [ - params.pcs.serialize_g1_uncompressed(require_commitment(c_b)), - params.pcs.serialize_g1_uncompressed(require_commitment(c_accip)), - params.pcs.serialize_g1_uncompressed(require_commitment(c_accx)), - params.pcs.serialize_g1_uncompressed(require_commitment(c_accy)), + params.pcs.serialize_g1_uncompressed(c_b.commitment), + params.pcs.serialize_g1_uncompressed(c_accip.commitment), + params.pcs.serialize_g1_uncompressed(c_accx.commitment), + params.pcs.serialize_g1_uncompressed(c_accy.commitment), ] - transcript = ring_root.verifier_transcript_prefix(params, self.transcript_challenge).copy() + transcript = ring_root.verifier_transcript_prefix(self.transcript_challenge).copy() return phase1_alphas_after_vk( transcript, relation_point, diff --git a/dot_ring/ring_proof/proof_payload.py b/dot_ring/ring_proof/proof_payload.py index 6ea9328..36de338 100644 --- a/dot_ring/ring_proof/proof_payload.py +++ b/dot_ring/ring_proof/proof_payload.py @@ -2,7 +2,7 @@ from dataclasses import dataclass -from dot_ring.ring_proof.columns.columns import Column, require_commitment +from dot_ring.ring_proof.columns.columns import Column from dot_ring.ring_proof.params import RingProofParams from dot_ring.ring_proof.pcs.protocol import G1Commitment @@ -71,10 +71,10 @@ def encoded_len(params: RingProofParams) -> int: def encode(self, params: RingProofParams) -> bytes: return b"".join( ( - params.pcs.compress_g1(require_commitment(self.c_b)), - params.pcs.compress_g1(require_commitment(self.c_accip)), - params.pcs.compress_g1(require_commitment(self.c_accx)), - params.pcs.compress_g1(require_commitment(self.c_accy)), + params.pcs.compress_g1(self.c_b.commitment), + params.pcs.compress_g1(self.c_accip.commitment), + params.pcs.compress_g1(self.c_accx.commitment), + params.pcs.compress_g1(self.c_accy.commitment), (self.px_zeta).to_bytes(RING_SCALAR_LEN, "little"), (self.py_zeta).to_bytes(RING_SCALAR_LEN, "little"), (self.s_zeta).to_bytes(RING_SCALAR_LEN, "little"), @@ -82,7 +82,7 @@ def encode(self, params: RingProofParams) -> bytes: (self.accip_zeta).to_bytes(RING_SCALAR_LEN, "little"), (self.accx_zeta).to_bytes(RING_SCALAR_LEN, "little"), (self.accy_zeta).to_bytes(RING_SCALAR_LEN, "little"), - params.pcs.compress_g1(require_commitment(self.c_q)), + params.pcs.compress_g1(self.c_q.commitment), (self.l_zeta_omega).to_bytes(RING_SCALAR_LEN, "little"), params.pcs.compress_g1(self.open_agg_zeta), params.pcs.compress_g1(self.open_l_zeta_omega), @@ -97,10 +97,10 @@ def decode(cls, proof: bytes, params: RingProofParams) -> RingProofPayload: reader = _PayloadReader(proof, params) payload = cls( - c_b=Column(name="c_b", evals=[], commitment=reader.commitment()), - c_accip=Column(name="c_accip", evals=[], commitment=reader.commitment()), - c_accx=Column(name="c_accx", evals=[], commitment=reader.commitment()), - c_accy=Column(name="c_accy", evals=[], commitment=reader.commitment()), + c_b=Column(name="c_b", evals=[], _commitment=reader.commitment()), + c_accip=Column(name="c_accip", evals=[], _commitment=reader.commitment()), + c_accx=Column(name="c_accx", evals=[], _commitment=reader.commitment()), + c_accy=Column(name="c_accy", evals=[], _commitment=reader.commitment()), px_zeta=reader.scalar(), py_zeta=reader.scalar(), s_zeta=reader.scalar(), @@ -108,7 +108,7 @@ def decode(cls, proof: bytes, params: RingProofParams) -> RingProofPayload: accip_zeta=reader.scalar(), accx_zeta=reader.scalar(), accy_zeta=reader.scalar(), - c_q=Column(name="c_q", evals=[], commitment=reader.commitment()), + c_q=Column(name="c_q", evals=[], _commitment=reader.commitment()), l_zeta_omega=reader.scalar(), open_agg_zeta=reader.commitment(), open_l_zeta_omega=reader.commitment(), diff --git a/dot_ring/ring_proof/verify.py b/dot_ring/ring_proof/verify.py index c7bbdc8..3d973a0 100644 --- a/dot_ring/ring_proof/verify.py +++ b/dot_ring/ring_proof/verify.py @@ -1,7 +1,7 @@ from typing import Any, NamedTuple from dot_ring.curve.point import CurvePoint -from dot_ring.ring_proof.params import ROOT_OF_UNITY_2048 +from dot_ring.ring_proof.params import ROOT_OF_UNITY_2048, RingProofParams from dot_ring.ring_proof.pcs.kzg import KZG from dot_ring.ring_proof.pcs.protocol import PCS from dot_ring.ring_proof.pcs.utils import ( @@ -48,73 +48,41 @@ def witness_commitments(self) -> tuple[Any, Any, Any, Any]: return (self.c_b, self.c_accip, self.c_accx, self.c_accy) -def _point_coords(point: CurvePoint) -> tuple[int, int]: - x, y = point.x, point.y - if x is None or y is None: - raise ValueError("identity point is not a valid ring proof instance") - return int(x), int(y) - - def _compute_quotient_and_linearization_terms( alpha_list: list[int], v_list: list[int], zeta_value: int, - px_value: int, - py_value: int, - s_value: int, - b_value: int, - accip_value: int, - accx_value: int, - accy_value: int, - l_zeta_omega_value: int, - seed: tuple[int, int], - result_seed: tuple[int, int], - domain_last_value: int, - domain_neg1_value: int, - domain_neg2_value: int, - domain_neg3_value: int, - domain_size: int, - domain_size_inv_value: int, + proof: RingProofFields, + seed: CurvePoint, + result_seed: CurvePoint, + domain: list[int], edwards_a_value: int, - omega_value: int, - prime_value: int, + omega: int, + prime: int, ) -> tuple[int, int, int, int, int, int]: if len(alpha_list) < 7 or len(v_list) < 8: raise ValueError("expected at least 7 alpha values and 8 aggregation values") - prime = int(prime_value) + domain_size_inv = pow(len(domain), -1, prime) + alphas = [int(value) % prime for value in alpha_list[:7]] v_values = [int(value) % prime for value in v_list[:8]] zeta = int(zeta_value) % prime - px = int(px_value) % prime - py = int(py_value) % prime - s = int(s_value) % prime - b = int(b_value) % prime - accip = int(accip_value) % prime - accx = int(accx_value) % prime - accy = int(accy_value) % prime - l_zeta_omega = int(l_zeta_omega_value) % prime - seed_x = int(seed[0]) % prime - seed_y = int(seed[1]) % prime - result_seed_x = int(result_seed[0]) % prime - result_seed_y = int(result_seed[1]) % prime - domain_last = int(domain_last_value) % prime - domain_neg1 = int(domain_neg1_value) % prime - domain_neg2 = int(domain_neg2_value) % prime - domain_neg3 = int(domain_neg3_value) % prime - inv_size = int(domain_size_inv_value) % prime - - zeta_n_minus_1 = (pow(zeta, domain_size, prime) - 1) % prime - zeta_minus_d4 = (zeta - domain_last) % prime + + zeta_n_minus_1 = (pow(zeta, len(domain), prime) - 1) % prime + zeta_minus_d4 = (zeta - domain[-4]) % prime zeta_minus_1 = (zeta - 1) % prime - zeta_minus_last = (zeta - domain_last) % prime + zeta_minus_last = (zeta - domain[-4]) % prime + + if seed.x is None or seed.y is None or result_seed.x is None or result_seed.y is None: + raise ValueError("seed and result_seed must be valid points with non-None coordinates") if zeta_minus_1 == 0: l0 = 1 inv_den = pow((zeta_minus_last * zeta_n_minus_1) % prime, -1, prime) inv_zeta_minus_last = (zeta_n_minus_1 * inv_den) % prime inv_zeta_n_minus_1 = (zeta_minus_last * inv_den) % prime - ln = domain_last * inv_size % prime + ln = domain[-4] * domain_size_inv % prime ln = ln * zeta_n_minus_1 % prime ln = ln * inv_zeta_minus_last % prime elif zeta_minus_last == 0: @@ -122,7 +90,7 @@ def _compute_quotient_and_linearization_terms( inv_den = pow((zeta_minus_1 * zeta_n_minus_1) % prime, -1, prime) inv_zeta_minus_1 = (zeta_n_minus_1 * inv_den) % prime inv_zeta_n_minus_1 = (zeta_minus_1 * inv_den) % prime - l0 = inv_size * zeta_n_minus_1 % prime + l0 = domain_size_inv * zeta_n_minus_1 % prime l0 = l0 * inv_zeta_minus_1 % prime else: den01 = zeta_minus_1 * zeta_minus_last % prime @@ -131,50 +99,49 @@ def _compute_quotient_and_linearization_terms( tmp = zeta_n_minus_1 * inv_den % prime inv_zeta_minus_1 = zeta_minus_last * tmp % prime inv_zeta_minus_last = zeta_minus_1 * tmp % prime - l0 = inv_size * zeta_n_minus_1 % prime + l0 = domain_size_inv * zeta_n_minus_1 % prime l0 = l0 * inv_zeta_minus_1 % prime - ln = domain_last * inv_size % prime + ln = domain[-4] * domain_size_inv % prime ln = ln * zeta_n_minus_1 % prime ln = ln * inv_zeta_minus_last % prime - one_minus_b = (1 - b) % prime - x1_y1 = accx * accy % prime - x2_y2 = px * py % prime + one_minus_b = (1 - proof.b_zeta) % prime + x1_y1 = proof.accx_zeta * proof.accy_zeta % prime + x2_y2 = proof.px_zeta * proof.py_zeta % prime c_values = [ - (-(accip + b * s) * zeta_minus_d4) % prime, - ((b * (-(x1_y1 + x2_y2)) + one_minus_b * (-accx)) * zeta_minus_d4) % prime, - ((b * (-(x1_y1 - x2_y2)) + one_minus_b * (-accy)) * zeta_minus_d4) % prime, - b * one_minus_b % prime, - ((accx - seed_x) * l0 + (accx - result_seed_x) * ln) % prime, - ((accy - seed_y) * l0 + (accy - result_seed_y) * ln) % prime, - (accip * l0 + (accip - 1) * ln) % prime, + (-(proof.accip_zeta + proof.b_zeta * proof.s_zeta) * zeta_minus_d4) % prime, + ((proof.b_zeta * (-(x1_y1 + x2_y2)) + one_minus_b * (-proof.accx_zeta)) * zeta_minus_d4) % prime, + ((proof.b_zeta * (-(x1_y1 - x2_y2)) + one_minus_b * (-proof.accy_zeta)) * zeta_minus_d4) % prime, + proof.b_zeta * one_minus_b % prime, + ((proof.accx_zeta - seed.x) * l0 + (proof.accx_zeta - result_seed.x) * ln) % prime, + ((proof.accy_zeta - seed.y) * l0 + (proof.accy_zeta - result_seed.y) * ln) % prime, + (proof.accip_zeta * l0 + (proof.accip_zeta - 1) * ln) % prime, ] linear_combination = sum((alpha * c_value) % prime for alpha, c_value in zip(alphas, c_values, strict=True)) % prime - prod_sum = (zeta - domain_neg1) % prime - prod_sum = prod_sum * ((zeta - domain_neg2) % prime) % prime - prod_sum = prod_sum * ((zeta - domain_neg3) % prime) % prime - q_zeta = (linear_combination + l_zeta_omega) % prime + prod_sum = (zeta - domain[-1]) % prime + prod_sum = prod_sum * ((zeta - domain[-2]) % prime) % prime + prod_sum = prod_sum * ((zeta - domain[-3]) % prime) % prime + q_zeta = (linear_combination + proof.l_zeta_omega) % prime q_zeta = q_zeta * prod_sum % prime q_zeta = q_zeta * inv_zeta_n_minus_1 % prime - terms = (px, py, s, b, accip, accx, accy, q_zeta) + terms = (proof.px_zeta, proof.py_zeta, proof.s_zeta, proof.b_zeta, proof.accip_zeta, proof.accx_zeta, proof.accy_zeta, q_zeta) agg_zeta = sum((v * term) % prime for v, term in zip(v_values, terms, strict=True)) % prime edwards_a = int(edwards_a_value) % prime - omega = int(omega_value) % prime - c_acc_x = b * ((accy * py + edwards_a * accx * px) % prime) % prime + c_acc_x = proof.b_zeta * ((proof.accy_zeta * proof.py_zeta + edwards_a * proof.accx_zeta * proof.px_zeta) % prime) % prime c_acc_x = (c_acc_x + one_minus_b) % prime scalar_accx = alphas[1] * (c_acc_x * zeta_minus_d4 % prime) % prime - c_acc_y = b * ((accx * py - px * accy) % prime) % prime + c_acc_y = proof.b_zeta * ((proof.accx_zeta * proof.py_zeta - proof.px_zeta * proof.accy_zeta) % prime) % prime c_acc_y = (c_acc_y + one_minus_b) % prime scalar_accy = alphas[2] * (c_acc_y * zeta_minus_d4 % prime) % prime scalar_accip = alphas[0] * zeta_minus_d4 % prime zeta_omega = zeta * omega % prime - return agg_zeta, scalar_accip, scalar_accx, scalar_accy, zeta_omega, l_zeta_omega + return agg_zeta, scalar_accip, scalar_accx, scalar_accy, zeta_omega, proof.l_zeta_omega def linear_pcs_verifications( @@ -183,18 +150,12 @@ def linear_pcs_verifications( relation_to_prove: CurvePoint, result_plus_seed: CurvePoint, seed_point: CurvePoint, - domain: list[int], - padding_rows: int, - domain_size_inv: int, - edwards_a: int, - omega: int, - prime: int, + params: RingProofParams, transcript_prefix: FiatShamirTranscript, witness_commitments: bytes, quotient_commitment: bytes, ) -> tuple[LinearPcsVerification, LinearPcsVerification]: - result_plus_seed_coords = _point_coords(result_plus_seed) - seed_coords = _point_coords(seed_point) + domain, a, omega, prime = params.domain, params.cv.curve.params.a, params.omega, params.prime _, alpha_list, zeta_p, v_list = derive_challenges_after_vk( transcript_prefix, @@ -205,28 +166,15 @@ def linear_pcs_verifications( proof.l_zeta_omega, ) - last_index = len(domain) - padding_rows linear_eval_bundle = _compute_quotient_and_linearization_terms( alpha_list, v_list, zeta_p, - proof.px_zeta, - proof.py_zeta, - proof.s_zeta, - proof.b_zeta, - proof.accip_zeta, - proof.accx_zeta, - proof.accy_zeta, - proof.l_zeta_omega, - seed_coords, - result_plus_seed_coords, - domain[last_index], - domain[-1], - domain[-2], - domain[-3], - len(domain), - domain_size_inv, - edwards_a, + proof, + seed_point, + result_plus_seed, + domain, + a, omega, prime, ) @@ -277,48 +225,31 @@ def __init__( prime: int | None = None, omega: int | None = None, pcs: type[PCS] = KZG, - domain_size_inv: int | None = None, transcript_witness_commitments: tuple[Any, ...] | bytes | None = None, transcript_quotient_commitment: Any | None = None, ) -> None: - self.Cb = proof.c_b - self.Caccip = proof.c_accip - self.Caccx = proof.c_accx - self.Caccy = proof.c_accy - self.px_zeta = proof.px_zeta - self.py_zeta = proof.py_zeta - self.s_zeta = proof.s_zeta - self.b_zeta = proof.b_zeta - self.accip_zeta = proof.accip_zeta - self.accx_zeta = proof.accx_zeta - self.accy_zeta = proof.accy_zeta - self.Cq = proof.c_q - self.l_zeta_omega = proof.l_zeta_omega - self.Phi_zeta = proof.phi_zeta - self.Phi_zeta_omega = proof.phi_zeta_omega + self.proof = proof self.Cpx, self.Cpy, self.Cs = fixed_cols self.relation_to_prove = relation_to_prove - self.Result_plus_Seed = _point_coords(result_plus_seed) - self.sp = _point_coords(seed_point) + self.Result_plus_Seed = result_plus_seed + self.sp = seed_point self.D = domain self.padding_rows = padding_rows self.prime = prime if prime is not None else relation_to_prove.curve.params.field_modulus self.omega = omega self.pcs = pcs - self.domain_size_inv = domain_size_inv if domain_size_inv is not None else pow(len(self.D), -1, self.prime) self.edwards_a = edwards_a if self.padding_rows < 1 or self.padding_rows >= len(self.D): raise ValueError("padding_rows must be >= 1 and less than domain size") - self.last_index = len(self.D) - self.padding_rows - self.Cb_blst = g1_to_blst(self.Cb) - self.Caccip_blst = g1_to_blst(self.Caccip) - self.Caccx_blst = g1_to_blst(self.Caccx) - self.Caccy_blst = g1_to_blst(self.Caccy) - self.Cq_blst = g1_to_blst(self.Cq) - self.Phi_zeta_blst = g1_to_blst(self.Phi_zeta) - self.Phi_zeta_omega_blst = g1_to_blst(self.Phi_zeta_omega) + self.Cb_blst = g1_to_blst(self.proof.c_b) + self.Caccip_blst = g1_to_blst(self.proof.c_accip) + self.Caccx_blst = g1_to_blst(self.proof.c_accx) + self.Caccy_blst = g1_to_blst(self.proof.c_accy) + self.Cq_blst = g1_to_blst(self.proof.c_q) + self.Phi_zeta_blst = g1_to_blst(self.proof.phi_zeta) + self.Phi_zeta_omega_blst = g1_to_blst(self.proof.phi_zeta_omega) self.Cpx_blst = g1_to_blst(self.Cpx) self.Cpy_blst = g1_to_blst(self.Cpy) @@ -360,7 +291,18 @@ def _linear_pcs_verifications( self, ) -> tuple[LinearPcsVerification, LinearPcsVerification]: """Prepare deferred linear KZG equations from one native scalar pass.""" - agg_zeta, scalar_accip, scalar_accx, scalar_accy, zeta_omega, l_zeta_omega = self._native_linear_eval_bundle() + agg_zeta, scalar_accip, scalar_accx, scalar_accy, zeta_omega, l_zeta_omega = _compute_quotient_and_linearization_terms( + self.alpha_list, + self.V_list, + self.zeta_p, + self.proof, + self.sp, + self.Result_plus_Seed, + self.D, + self.edwards_a, + self._domain_omega(), + self.prime, + ) quotient_terms = ( (self.Cpx_blst, self.V_list[0]), (self.Cpy_blst, self.V_list[1]), @@ -380,31 +322,3 @@ def _linear_pcs_verifications( LinearPcsVerification(quotient_terms, self.Phi_zeta_blst, self.zeta_p, agg_zeta), LinearPcsVerification(linearization_terms, self.Phi_zeta_omega_blst, zeta_omega, l_zeta_omega), ) - - def _native_linear_eval_bundle(self) -> tuple[int, int, int, int, int, int]: - omega = self._domain_omega() - bundle = _compute_quotient_and_linearization_terms( - self.alpha_list, - self.V_list, - self.zeta_p, - self.px_zeta, - self.py_zeta, - self.s_zeta, - self.b_zeta, - self.accip_zeta, - self.accx_zeta, - self.accy_zeta, - self.l_zeta_omega, - self.sp, - self.Result_plus_Seed, - self.D[self.last_index], - self.D[-1], - self.D[-2], - self.D[-3], - len(self.D), - self.domain_size_inv, - self.edwards_a, - omega, - self.prime, - ) - return bundle diff --git a/dot_ring/vrf/codec.py b/dot_ring/vrf/codec.py index b54d285..66b07ed 100644 --- a/dot_ring/vrf/codec.py +++ b/dot_ring/vrf/codec.py @@ -2,11 +2,8 @@ from __future__ import annotations -from functools import lru_cache - from dot_ring.curve.curve import CurveVariant from dot_ring.curve.point import CurvePoint -from dot_ring.curve.twisted_edwards.te_affine_point import TEAffinePoint def scalar_len(curve: CurveVariant) -> int: @@ -43,7 +40,7 @@ def dec_point(curve: CurveVariant, value: bytes) -> CurvePoint: if len(value) != point_len(curve): raise ValueError(f"point must be exactly {point_len(curve)} bytes") point = curve.point_type.string_to_point(value) - if not valid_point(point): + if not curve.curve.valid_point(point): raise ValueError("point is not a valid nonidentity subgroup point") return point @@ -52,26 +49,3 @@ def enc_64(value: int) -> bytes: if not 0 <= value < 1 << 64: raise ValueError("value does not fit in uint64") return value.to_bytes(8, "little") - - -@lru_cache(maxsize=32) -def _cofactor_inverse(cofactor: int, subgroup_order: int) -> int: - return pow(cofactor, -1, subgroup_order) - - -def valid_point(point: CurvePoint) -> bool: - if point.is_identity(): - return False - - cofactor = point.curve.params.cofactor - if cofactor == 1: - return True - - cofactor_inv = _cofactor_inverse(cofactor, point.curve.params.subgroup_order) - if isinstance(point, TEAffinePoint): - cleared = TEAffinePoint.__mul__(point, cofactor) - else: - cleared = point * cofactor - if cleared.is_identity(): - return False - return cleared * cofactor_inv == point diff --git a/dot_ring/vrf/ietf/thin.py b/dot_ring/vrf/ietf/thin.py index 516d16f..22d7e6b 100644 --- a/dot_ring/vrf/ietf/thin.py +++ b/dot_ring/vrf/ietf/thin.py @@ -10,7 +10,7 @@ from typing import Any from dot_ring.curve.point import CurvePoint -from dot_ring.vrf.codec import dec_scalar, dec_scalar_mod, enc_point, enc_scalar, point_len, scalar_len, valid_point +from dot_ring.vrf.codec import dec_point, dec_scalar, dec_scalar_mod, enc_point, enc_scalar, point_len, scalar_len from dot_ring.vrf.domain import DomSep from dot_ring.vrf.primitives import ( CHALLENGE_LEN, @@ -50,15 +50,10 @@ def decode(cls, proof_bytes: bytes) -> ThinVRF: expected = 2 * encoded_point_len + scalar_size if len(proof_bytes) != expected: raise ValueError(f"invalid Thin VRF proof length: expected {expected}, got {len(proof_bytes)}") - try: - output_point = cls.cv.point_type.string_to_point(proof_bytes[:encoded_point_len]) - r = cls.cv.point_type.string_to_point(proof_bytes[encoded_point_len : 2 * encoded_point_len]) - except ValueError as exc: - raise ValueError("Invalid point in proof") from exc + o = dec_point(cls.cv, proof_bytes[:encoded_point_len]) + r = dec_point(cls.cv, proof_bytes[encoded_point_len : 2 * encoded_point_len]) s = dec_scalar(cls.cv, proof_bytes[2 * encoded_point_len :]) - if not (valid_point(output_point) and valid_point(r)): - raise ValueError("Invalid identity or subgroup point in proof") - return cls(output_point, r, s) + return cls(o, r, s) def encode(self) -> bytes: return enc_point(self.output_point) + enc_point(self.r) + enc_scalar(self.cv, self.s) @@ -90,11 +85,10 @@ def prove( def verify(self, public_key: bytes, input: bytes, additional_data: bytes, salt: bytes = b"") -> bool: input_point = self.cv.point_type.encode_to_curve(input, salt) try: - public_key_point = self.cv.point_type.string_to_point(public_key) + public_key_point = dec_point(self.cv, public_key) except ValueError as exc: raise ValueError("Invalid public key") from exc - if not (valid_point(public_key_point) and valid_point(self.r) and valid_point(input_point) and valid_point(self.output_point)): - return False + transcript, merged = vrf_transcript( self.cv, DomSep.THIN_VRF, @@ -126,9 +120,7 @@ def batch_verify( try: for proof, public_key, input_value, ad, salt in zip(proofs, public_keys, inputs, additional_data, salts, strict=True): input_point = cls.cv.point_type.encode_to_curve(input_value, salt) - public_key_point = cls.cv.point_type.string_to_point(public_key) - if not (valid_point(public_key_point) and valid_point(proof.r) and valid_point(input_point) and valid_point(proof.output_point)): - return False + public_key_point = dec_point(cls.cv, public_key) ios = [VrfIo(cls.cv.point_type.generator_point(), public_key_point), VrfIo(input_point, proof.output_point)] transcript, scalars = vrf_transcript_scalars(cls.cv, DomSep.THIN_VRF, ios, ad) items.append(_ThinBatchItem(challenge(cls.cv, [proof.r], transcript), ios, scalars, proof.r, proof.s)) diff --git a/dot_ring/vrf/ietf/tiny.py b/dot_ring/vrf/ietf/tiny.py index faefb21..646d2c8 100644 --- a/dot_ring/vrf/ietf/tiny.py +++ b/dot_ring/vrf/ietf/tiny.py @@ -9,7 +9,7 @@ from typing import Any from dot_ring.curve.point import CurvePoint -from dot_ring.vrf.codec import dec_scalar, dec_scalar_mod, enc_point, enc_scalar, point_len, scalar_len, valid_point +from dot_ring.vrf.codec import dec_point, dec_scalar, dec_scalar_mod, enc_point, enc_scalar, point_len, scalar_len from dot_ring.vrf.domain import DomSep from dot_ring.vrf.primitives import ( CHALLENGE_LEN, @@ -39,13 +39,11 @@ def decode(cls, proof_bytes: bytes) -> TinyVRF: if len(proof_bytes) != expected: raise ValueError(f"invalid Tiny VRF proof length: expected {expected}, got {len(proof_bytes)}") try: - output_point = cls.cv.point_type.string_to_point(proof_bytes[:encoded_point_len]) + output_point = dec_point(cls.cv, proof_bytes[:encoded_point_len]) except ValueError as exc: raise ValueError("Invalid output point") from exc c = dec_scalar_mod(cls.cv, proof_bytes[encoded_point_len : encoded_point_len + CHALLENGE_LEN]) s = dec_scalar(cls.cv, proof_bytes[encoded_point_len + CHALLENGE_LEN :]) - if not valid_point(output_point): - raise ValueError("Invalid identity or subgroup point in proof") return cls(output_point, c, s) def encode(self) -> bytes: @@ -74,12 +72,10 @@ def prove( def verify(self, public_key: bytes, input: bytes, additional_data: bytes, salt: bytes = b"") -> bool: input_point = self.cv.point_type.encode_to_curve(input, salt) try: - public_key_pt = self.cv.point_type.string_to_point(public_key) + public_key_pt = dec_point(self.cv, public_key) except ValueError as exc: raise ValueError("Invalid public key") from exc - if not valid_point(public_key_pt) or not valid_point(input_point) or not valid_point(self.output_point): - return False ios = [VrfIo(self.cv.point_type.generator_point(), public_key_pt), VrfIo(input_point, self.output_point)] transcript, merged = vrf_transcript(self.cv, DomSep.TINY_VRF, ios, additional_data) r = self.cv.point_type.msm([merged.input, merged.output], [self.s, -self.c]) @@ -91,30 +87,3 @@ def proof_to_hash(cls, gamma: CurvePoint, mul_cofactor: bool = False) -> bytes: if mul_cofactor: gamma = gamma * cls.cv.curve.params.cofactor return point_to_hash(cls.cv, gamma) - - @classmethod - def ecvrf_decode_proof(cls, proof: bytes | str) -> tuple[CurvePoint, int, int]: - if not isinstance(proof, bytes): - proof = bytes.fromhex(proof) - - encoded_point_len = point_len(cls.cv) - scalar_size = scalar_len(cls.cv) - expected = encoded_point_len + CHALLENGE_LEN + scalar_size - if len(proof) != expected: - raise ValueError(f"invalid Tiny VRF proof length: expected {expected}, got {len(proof)}") - - try: - output_point = cls.cv.point_type.string_to_point(proof[:encoded_point_len]) - except ValueError as exc: - raise ValueError("Invalid gamma point") from exc - - c = dec_scalar_mod(cls.cv, proof[encoded_point_len : encoded_point_len + CHALLENGE_LEN]) - s = dec_scalar(cls.cv, proof[encoded_point_len + CHALLENGE_LEN :]) - if not valid_point(output_point): - raise ValueError("Invalid identity or subgroup point in proof") - return output_point, c, s - - @classmethod - def ecvrf_proof_to_hash(cls, proof: bytes | str) -> bytes: - output_point, _, _ = cls.ecvrf_decode_proof(proof) - return cls.proof_to_hash(output_point) diff --git a/dot_ring/vrf/pedersen/vrf.py b/dot_ring/vrf/pedersen/vrf.py index f1c52b8..aa0bb8d 100644 --- a/dot_ring/vrf/pedersen/vrf.py +++ b/dot_ring/vrf/pedersen/vrf.py @@ -7,12 +7,12 @@ from __future__ import annotations from collections.abc import Sequence -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any, TypeVar from dot_ring.curve.curve import CurveVariant from dot_ring.curve.point import CurvePoint -from dot_ring.vrf.codec import dec_scalar, dec_scalar_mod, enc_64, enc_point, enc_scalar, point_len, scalar_len, valid_point +from dot_ring.vrf.codec import dec_point, dec_scalar, dec_scalar_mod, enc_point, enc_scalar, point_len, scalar_len from dot_ring.vrf.domain import DomSep from dot_ring.vrf.primitives import ( CHALLENGE_LEN, @@ -39,7 +39,6 @@ class PedersenVRF(VRF[C]): s: int sb: int _blinding_factor: int = 0 - _points_validated: bool = field(default=False, repr=False, compare=False) @classmethod def proof_len(cls) -> int: @@ -55,16 +54,14 @@ def decode(cls, proof: bytes) -> PedersenVRF: raise ValueError(f"invalid Pedersen VRF proof length: expected {expected}, got {len(proof)}") try: - output_point = cls.cv.point_type.string_to_point(proof[0:point_length]) - public_key_cp = cls.cv.point_type.string_to_point(proof[point_length : 2 * point_length]) - r = cls.cv.point_type.string_to_point(proof[2 * point_length : 3 * point_length]) - ok = cls.cv.point_type.string_to_point(proof[3 * point_length : 4 * point_length]) + output_point = dec_point(cls.cv, proof[0:point_length]) + public_key_cp = dec_point(cls.cv, proof[point_length : 2 * point_length]) + r = dec_point(cls.cv, proof[2 * point_length : 3 * point_length]) + ok = dec_point(cls.cv, proof[3 * point_length : 4 * point_length]) except ValueError as exc: raise ValueError("Invalid point in proof") from exc s = dec_scalar(cls.cv, proof[4 * point_length : 4 * point_length + scalar_size]) sb = dec_scalar(cls.cv, proof[4 * point_length + scalar_size :]) - if not all(valid_point(point) for point in (output_point, public_key_cp, r, ok)): - raise ValueError("Invalid identity or subgroup point in proof") return cls( output_point=output_point, @@ -73,7 +70,6 @@ def decode(cls, proof: bytes) -> PedersenVRF: ok=ok, s=s, sb=sb, - _points_validated=True, ) def encode(self) -> bytes: @@ -127,12 +123,9 @@ def prove( s=s, sb=sb, _blinding_factor=blinding_factor, - _points_validated=True, ) def verify(self, input: bytes, additional_data: bytes, salt: bytes = b"") -> bool: - if not self._valid_proof_points(): - return False input_point = self.cv.point_type.encode_to_curve(input, salt) transcript, merged = vrf_transcript(self.cv, DomSep.PEDERSEN_VRF, [VrfIo(input_point, self.output_point)], additional_data) transcript.absorb(enc_point(self.blinded_pk)) @@ -149,42 +142,18 @@ def verify(self, input: bytes, additional_data: bytes, salt: bytes = b"") -> boo lhs2 = self.cv.point_type.msm([generator, blinding_base, self.blinded_pk], [self.s, self.sb, -c]) return lhs2 == self.result_point - def _valid_proof_points(self) -> bool: - if self._points_validated: - return True - return all(valid_point(point) for point in (self.output_point, self.blinded_pk, self.result_point, self.ok)) - - @classmethod - def _valid_ios(cls, ios: list[VrfIo]) -> bool: - seen: set[int] = set() - for io in ios: - for point in (io.input, io.output): - point_id = id(point) - if point_id in seen: - continue - seen.add(point_id) - if not valid_point(point): - return False - return True - - def verify_unblinding(self, public_key: bytes | str | CurvePoint, blinding_factor: int) -> bool: + def verify_unblinding(self, public_key: bytes, blinding_factor: int) -> bool: """Spec section 4.3: check `Y_bar = Y + b*B` for a revealed blinding factor.""" order = self.cv.curve.params.subgroup_order if not 0 <= blinding_factor < order: return False - if isinstance(public_key, bytes | str): - try: - public_key = self.cv.point_type.string_to_point(public_key) - except ValueError as exc: - raise ValueError("Invalid public key") from exc - blinded_pk_valid = self._points_validated or valid_point(self.blinded_pk) - if not (valid_point(public_key) and blinded_pk_valid): - return False + + public = dec_point(self.cv, public_key) if not self.cv.curve.params.auxiliary_points.blinding_base: raise ValueError("Curve does not have a blinding base point for Pedersen VRF") - blinding_base = self.cv.point_type(*self.cv.curve.params.auxiliary_points.blinding_base) + blinding_base = self.cv.point(self.cv.curve.params.auxiliary_points.blinding_base) - return public_key + blinding_base * blinding_factor == self.blinded_pk + return public + blinding_base * blinding_factor == self.blinded_pk @classmethod def blinding_scalar(cls, secret_scalar: int, transcript: Any) -> int: @@ -198,16 +167,6 @@ def proof_to_hash(cls, gamma: CurvePoint, mul_cofactor: bool = False) -> bytes: gamma = gamma * cls.cv.curve.params.cofactor return point_to_hash(cls.cv, gamma) - @classmethod - def ecvrf_proof_to_hash(cls, output_point_bytes: bytes | str) -> bytes: - if not isinstance(output_point_bytes, bytes): - output_point_bytes = bytes.fromhex(output_point_bytes) - try: - output_point = cls.cv.point_type.string_to_point(output_point_bytes) - except ValueError as exc: - raise ValueError("Invalid output point") from exc - return cls.proof_to_hash(output_point) - @classmethod def batch_verify( cls, @@ -219,34 +178,17 @@ def batch_verify( if salts is None: salts = [b""] * len(proofs) - input_points: list[CurvePoint] = [] - try: - for input_value, salt in zip(inputs, salts, strict=True): - input_points.append(cls.cv.point_type.encode_to_curve(input_value, salt)) - except (AttributeError, TypeError, ValueError): - return False - batch_items: list[tuple[PedersenVRF, CurvePoint, int]] = [] order = cls.cv.curve.params.subgroup_order half_order = order >> 1 + batch_items: list[tuple[PedersenVRF, CurvePoint, int]] = [] coefficient_item_bytes = bytearray() try: - for proof, input_point, ad in zip(proofs, input_points, additional_data, strict=True): - if proof.cv.name != cls.cv.name or not proof._valid_proof_points(): - return False - - absorbed = bytearray(cls.cv.curve.params.suite_id) - absorbed.append(DomSep.PEDERSEN_VRF) - absorbed.extend(enc_64(1)) - absorbed.extend(enc_point(input_point)) - absorbed.extend(enc_point(proof.output_point)) - absorbed.extend(enc_64(len(ad))) - absorbed.extend(ad) - absorbed.extend(enc_point(proof.blinded_pk)) - absorbed.append(DomSep.CHALLENGE) - absorbed.extend(enc_point(proof.result_point)) - absorbed.extend(enc_point(proof.ok)) - c = dec_scalar_mod(cls.cv, squeeze_transcript_bytes(cls.cv.curve.params.hash_fn, bytes(absorbed), CHALLENGE_LEN)) + for proof, input_value, ad, salt in zip(proofs, inputs, additional_data, salts, strict=True): + input_point = cls.cv.point_type.encode_to_curve(input_value, salt) + transcript, _ = vrf_transcript(cls.cv, DomSep.PEDERSEN_VRF, [VrfIo(input_point, proof.output_point)], ad) + transcript.absorb(enc_point(proof.blinded_pk)) + c = challenge(cls.cv, [proof.result_point, proof.ok], transcript) batch_items.append((proof, input_point, c)) coefficient_item_bytes.extend(enc_scalar(cls.cv, c)) @@ -258,107 +200,43 @@ def batch_verify( if not batch_items: return True + if not cls.cv.curve.params.auxiliary_points.blinding_base: + raise ValueError("Curve does not have a blinding base point for Pedersen VRF") + blinding_base = cls.cv.point_type(*cls.cv.curve.params.auxiliary_points.blinding_base) + + def signed(scalar: int) -> int: + scalar %= order + return scalar - order if scalar > half_order else scalar + absorbed = bytearray(cls.cv.curve.params.suite_id) absorbed.append(DomSep.BATCH_VERIFY) absorbed.extend(coefficient_item_bytes) weights = squeeze_transcript_bytes(cls.cv.curve.params.hash_fn, bytes(absorbed), 2 * CHALLENGE_LEN * len(batch_items)) - point_count = len(batch_items) * 5 + 2 - points: list[CurvePoint | None] = [None] * point_count - scalars = [0] * point_count + points: list[CurvePoint] = [] + scalars: list[int] = [] generator_scalar = 0 blinding_scalar = 0 - pos = 0 for index, (proof, input_point, c) in enumerate(batch_items): offset = 2 * CHALLENGE_LEN * index io_weight = dec_scalar_mod(cls.cv, weights[offset : offset + CHALLENGE_LEN]) commitment_weight = dec_scalar_mod(cls.cv, weights[offset + CHALLENGE_LEN : offset + 2 * CHALLENGE_LEN]) - value = (io_weight * proof.s) % order - if value > half_order: - value -= order - points[pos] = input_point - scalars[pos] = value - pos += 1 - - value = -(io_weight * c) % order - if value > half_order: - value -= order - points[pos] = proof.output_point - scalars[pos] = value - pos += 1 - - points[pos] = proof.ok - scalars[pos] = -io_weight - pos += 1 - - generator_scalar = (generator_scalar + commitment_weight * proof.s) % order - blinding_scalar = (blinding_scalar + commitment_weight * proof.sb) % order - - value = -(commitment_weight * c) % order - if value > half_order: - value -= order - points[pos] = proof.blinded_pk - scalars[pos] = value - pos += 1 - - points[pos] = proof.result_point - scalars[pos] = -commitment_weight - pos += 1 + points.extend((proof.ok, proof.output_point, input_point)) + scalars.extend((io_weight, signed(io_weight * c), signed(-io_weight * proof.s))) - if not cls.cv.curve.params.auxiliary_points.blinding_base: - raise ValueError("Curve does not have a blinding base point for Pedersen VRF") - blinding_base = cls.cv.point_type(*cls.cv.curve.params.auxiliary_points.blinding_base) + generator_scalar = (generator_scalar - commitment_weight * proof.s) % order + blinding_scalar = (blinding_scalar - commitment_weight * proof.sb) % order + + points.extend((proof.result_point, proof.blinded_pk)) + scalars.extend((commitment_weight, signed(commitment_weight * c))) if generator_scalar: - value = generator_scalar - if value > half_order: - value -= order - points[pos] = cls.cv.point_type.generator_point() - scalars[pos] = value - pos += 1 + points.append(cls.cv.point_type.generator_point()) + scalars.append(signed(generator_scalar)) if blinding_scalar: - value = blinding_scalar - if value > half_order: - value -= order - points[pos] = blinding_base - scalars[pos] = value - pos += 1 - - folded_points = [point for point in points[:pos] if point is not None] - folded_scalars = scalars[:pos] - seen: set[int] = set() - has_duplicate = False - for point in folded_points: - point_id = id(point) - if point_id in seen: - has_duplicate = True - break - seen.add(point_id) - - if has_duplicate: - points_by_id: dict[int, CurvePoint] = {} - scalars_by_id: dict[int, int] = {} - for point, scalar in zip(folded_points, folded_scalars, strict=True): - scalar %= order - if scalar == 0: - continue - point_id = id(point) - if point_id in scalars_by_id: - scalars_by_id[point_id] = (scalars_by_id[point_id] + scalar) % order - else: - points_by_id[point_id] = point - scalars_by_id[point_id] = scalar - - folded_points = [] - folded_scalars = [] - for point_id, point in points_by_id.items(): - scalar = scalars_by_id[point_id] % order - if scalar > half_order: - scalar -= order - if scalar: - folded_points.append(point) - folded_scalars.append(scalar) - - return cls.cv.point_type.msm(folded_points, folded_scalars).is_identity() + points.append(blinding_base) + scalars.append(signed(blinding_scalar)) + + return cls.cv.point_type.msm(points, scalars).is_identity() diff --git a/dot_ring/vrf/ring/members.py b/dot_ring/vrf/ring/members.py index 89154a6..4bff13a 100644 --- a/dot_ring/vrf/ring/members.py +++ b/dot_ring/vrf/ring/members.py @@ -7,24 +7,19 @@ from __future__ import annotations from collections.abc import Sequence +from functools import lru_cache -from dot_ring.curve.curve import CurveVariant from dot_ring.ring_proof.params import RingProofParams -from dot_ring.vrf.codec import point_len +from dot_ring.vrf.codec import dec_point - -def parse_concatenated_keys(keys: bytes, cv: CurveVariant) -> list[bytes]: - encoded_point_len = point_len(cv) - if len(keys) % encoded_point_len != 0: - raise ValueError(f"invalid concatenated key length: expected multiple of {encoded_point_len}, got {len(keys)}") - return [keys[encoded_point_len * i : encoded_point_len * (i + 1)] for i in range(len(keys) // encoded_point_len)] +ParamsCacheKey = tuple[int, int, int, int | None, int, int, bool, int, int] class Ring: nm_points: tuple[tuple[int, int], ...] params: RingProofParams - def __init__(self, keys: Sequence[bytes | str], params: RingProofParams | None = None) -> None: + def __init__(self, keys: Sequence[bytes], params: RingProofParams | None = None) -> None: """Build spec public vector `PK || B powers || zero padding` from encoded keys.""" if params is None: params = RingProofParams.from_ring_size(len(keys)) @@ -39,7 +34,7 @@ def __init__(self, keys: Sequence[bytes | str], params: RingProofParams | None = nm_points: list[tuple[int, int]] = [] for key in keys: - point = self._decode_key(key, padding_point) + point = self._decode_key(key) if point is None: nm_points.append(padding_point) continue @@ -50,26 +45,32 @@ def __init__(self, keys: Sequence[bytes | str], params: RingProofParams | None = fill_count = params.domain_size - params.padding_rows - len(nm_points) if fill_count > 0: - nm_points.extend(blinding_base_powers(params, fill_count)) + blinding_base = params.cv.curve.params.auxiliary_points.blinding_base + if not blinding_base: + raise ValueError("blinding base is not configured in curve parameters") + nm_points.extend(_blinding_base_powers(params.cv.point_type, blinding_base, fill_count)) if params.padding_rows > 0: nm_points.extend([(0, 0)] * params.padding_rows) self.nm_points = tuple(nm_points) - def _decode_key(self, key: object, padding_point: tuple[int, int]) -> tuple[int, int] | None: - if not isinstance(key, (bytes, str)): - return None + @classmethod + def from_keys(cls, keys: Sequence[bytes], params: RingProofParams | None = None) -> Ring: + """Return a cached ring for a stable encoded key vector.""" + return _ring(tuple(bytes(key) for key in keys)) + + def _decode_key(self, key: bytes) -> tuple[int, int] | None: try: - point = self.params.cv.point_type.string_to_point(key) + point = dec_point(self.params.cv, key) except ValueError: return None if point.is_identity(): - return padding_point + return None return int(point.x), int(point.y) - def index_of(self, key: bytes | str) -> int: + def index_of(self, key: bytes) -> int: padding_point = self.params.cv.curve.params.auxiliary_points.padding_point - point = self._decode_key(key, padding_point) + point = self._decode_key(key) if point is None: raise ValueError("invalid ring key") if point == padding_point: @@ -80,12 +81,21 @@ def index_of(self, key: bytes | str) -> int: raise ValueError("producer key is not in ring") from exc -def blinding_base_powers(params: RingProofParams, count: int) -> tuple[tuple[int, int], ...]: - point = params.cv.curve.params.auxiliary_points.blinding_base - points = [] +@lru_cache(maxsize=8) +def _blinding_base_powers(point_type: type, blinding_base: tuple[int, int], count: int) -> tuple[tuple[int, int], ...]: + points: list[tuple[int, int]] = [] + point = point_type(*blinding_base) for _ in range(count): - points.append(point) - point_obj = params.cv.point_type(*point) - doubled = point_obj + point_obj - point = int(doubled.x), int(doubled.y) + points.append((int(point.x), int(point.y))) + point = point + point return tuple(points) + + +@lru_cache(maxsize=2) +def _params(keys_len: int) -> RingProofParams: + return RingProofParams.from_ring_size(keys_len) + + +@lru_cache(maxsize=8) +def _ring(keys: tuple[bytes, ...]) -> Ring: + return Ring(keys, _params(len(keys))) diff --git a/dot_ring/vrf/ring/root.py b/dot_ring/vrf/ring/root.py index 4c33346..76510fe 100644 --- a/dot_ring/vrf/ring/root.py +++ b/dot_ring/vrf/ring/root.py @@ -2,42 +2,12 @@ from functools import lru_cache from typing import Any -from dot_ring.ring_proof.columns.columns import Column, require_commitment +from dot_ring.ring_proof.columns.columns import Column from dot_ring.ring_proof.params import RingProofParams from dot_ring.ring_proof.pcs.kzg import KZG from dot_ring.ring_proof.transcript.phases import serialize_verifier_key from dot_ring.ring_proof.transcript.transcript import FiatShamirTranscript - -from .members import Ring - - -# Note: [Assumption] Selector column would likely be same as long as ring size doesn't change -@lru_cache(maxsize=4) -def _selector_column_data( - domain_size: int, - max_ring_size: int, - omega: int, - prime: int, - pcs: Any, -) -> tuple[tuple[int, ...], tuple[int, ...], Any]: - selector_vec = [1 if i < max_ring_size else 0 for i in range(domain_size)] - selector_col = Column("s", selector_vec, size=domain_size) - selector_col.interpolate(omega, prime) - selector_col.commit(pcs) - if selector_col.coeffs is None or selector_col.commitment is None: - raise ValueError("failed to build selector column") - return tuple(selector_col.evals), tuple(selector_col.coeffs), selector_col.commitment - - -def _selector_column(params: RingProofParams) -> Column: - evals, coeffs, commitment = _selector_column_data( - params.domain_size, - params.max_ring_size, - params.omega, - params.prime, - params.pcs, - ) - return Column("s", list(evals), list(coeffs), commitment=commitment.dup(), size=params.domain_size) +from dot_ring.vrf.ring.members import Ring @dataclass @@ -51,47 +21,52 @@ class RingRoot: def from_ring(cls, ring: Ring, params: RingProofParams | None = None): if params is None: params = ring.params - px = [point[0] for point in ring.nm_points] - py = [point[1] for point in ring.nm_points] - px_col = Column("Px", px, size=params.domain_size) - py_col = Column("Py", py, size=params.domain_size) - s_col = _selector_column(params) - for col in (px_col, py_col): - col.interpolate(params.omega, params.prime) - col.commit(params.pcs) - return cls(px=px_col, py=py_col, s=s_col, params=params) - - def fixed_commitments(self, params: RingProofParams | None = None) -> list[Any]: - """Return fixed-column commitments for the ring root.""" - if params is None: - params = self.params - if params is None: - raise ValueError("Ring root commitments require ring proof parameters") + s_evals, s_coeffs, s_commitment = _selector_column_data( + domain_size=params.domain_size, + max_ring_size=params.max_ring_size, + omega=params.omega, + prime=params.prime, + pcs=params.pcs, + ) + s = Column("s", list(s_evals), coeffs=list(s_coeffs), _commitment=s_commitment, size=params.domain_size) + + px_evals, px_coeffs, px_commitment, py_evals, py_coeffs, py_commitment = _public_keys_column_data( + nm_points=ring.nm_points, + domain_size=params.domain_size, + omega=params.omega, + prime=params.prime, + pcs=params.pcs, + ) + px = Column("px", list(px_evals), coeffs=list(px_coeffs), _commitment=px_commitment, size=params.domain_size) + py = Column("py", list(py_evals), coeffs=list(py_coeffs), _commitment=py_commitment, size=params.domain_size) + + return cls(px=px, py=py, s=s, params=params) + + def fixed_commitments(self) -> list[Any]: + """Return fixed-column commitments for the ring root.""" return [ - require_commitment(self.px), - require_commitment(self.py), - require_commitment(self.s), + self.px.commitment, + self.py.commitment, + self.s.commitment, ] - def verifier_transcript_prefix(self, params: RingProofParams | None = None, transcript_challenge: bytes | None = None): + def verifier_transcript_prefix(self, transcript_challenge: bytes | None = None): """Return a transcript state after absorbing the fixed verifier key.""" - if params is None: - params = self.params - if params is None: + if self.params is None: raise ValueError("Ring root verifier transcript requires ring proof parameters") - commitments = self.fixed_commitments(params) - commitment_bytes = [params.pcs.serialize_g1_uncompressed(commitment) for commitment in commitments] + commitments = self.fixed_commitments() + commitment_bytes = [self.params.pcs.serialize_g1_uncompressed(commitment) for commitment in commitments] verifier_key_bytes = serialize_verifier_key( - params.pcs.srs.g1_points[0], - [(b, a) for pair in params.pcs.srs.g2_points for point in pair for a, b in [point]], + self.params.pcs.srs.g1_points[0], + [(b, a) for pair in self.params.pcs.srs.g2_points for point in pair for a, b in [point]], commitment_bytes, ) if transcript_challenge is None: - transcript_challenge = params.cv.curve.params.suite_id + transcript_challenge = self.params.cv.curve.params.suite_id - transcript = FiatShamirTranscript(params.prime, transcript_challenge) + transcript = FiatShamirTranscript(self.params.prime, transcript_challenge) transcript.absorb_labeled(b"vk", verifier_key_bytes) return transcript @@ -104,47 +79,37 @@ def encode(self) -> bytes: pcs = self.params.pcs if self.params is not None else KZG return b"".join( ( - pcs.compress_g1(require_commitment(self.px)), - pcs.compress_g1(require_commitment(self.py)), - pcs.compress_g1(require_commitment(self.s)), + pcs.compress_g1(self.px.commitment), + pcs.compress_g1(self.py.commitment), + pcs.compress_g1(self.s.commitment), ) ) @classmethod - def decode(cls, data: bytes, params: RingProofParams | None = None) -> "RingRoot": - commitment_size = params.pcs.commitment_size if params is not None else KZG.commitment_size + def decode(cls, data: bytes, ring: Ring | RingProofParams | None = None) -> "RingRoot": + params = ring.params if isinstance(ring, Ring) else ring + if params is None: + params = RingProofParams() + commitment_size = params.pcs.commitment_size expected = cls.encoded_len(params) if len(data) != expected: raise ValueError(f"invalid ring root length: ring root must be exactly {expected} bytes, got {len(data)}") - pcs = params.pcs if params is not None else KZG + pcs = params.pcs reader = _RingRootReader(data, commitment_size) px_commitment = reader.g1(pcs) py_commitment = reader.g1(pcs) s_commitment = reader.g1(pcs) reader.finish() - if params is None: - px = Column("px", [], commitment=px_commitment) - py = Column("py", [], commitment=py_commitment) - s = Column("s", [], commitment=s_commitment) - else: - px = Column("px", [], commitment=px_commitment, size=params.domain_size) - py = Column("py", [], commitment=py_commitment, size=params.domain_size) - s = Column("s", [], commitment=s_commitment, size=params.domain_size) + px = Column("px", [], _commitment=px_commitment, size=params.domain_size) + py = Column("py", [], _commitment=py_commitment, size=params.domain_size) + s = Column("s", [], _commitment=s_commitment, size=params.domain_size) return cls(px=px, py=py, s=s, params=params) def matches_ring(self, ring: Ring) -> bool: - params = ring.params - domain_size = params.domain_size - if len(self.px.evals) >= domain_size and len(self.py.evals) >= domain_size and len(self.s.evals) >= domain_size: - return ( - tuple(self.px.evals[:domain_size]) == tuple(point[0] for point in ring.nm_points) - and tuple(self.py.evals[:domain_size]) == tuple(point[1] for point in ring.nm_points) - and tuple(self.s.evals[:domain_size]) == tuple(1 if i < params.max_ring_size else 0 for i in range(domain_size)) - ) - return RingRoot.from_ring(ring, params).encode() == self.encode() + return RingRoot.from_ring(ring).encode() == self.encode() class _RingRootReader: @@ -162,3 +127,47 @@ def g1(self, pcs: Any) -> Any: def finish(self) -> None: if self.offset != len(self.data): raise ValueError(f"trailing bytes in ring root: {len(self.data) - self.offset}") + + +# Assumption: Selector column would likely be same as long as ring size doesn't change +@lru_cache(maxsize=2) +def _selector_column_data( + domain_size: int, + max_ring_size: int, + omega: int, + prime: int, + pcs: Any, +) -> tuple[tuple[int, ...], tuple[int, ...], Any]: + selector_vec = [1 if i < max_ring_size else 0 for i in range(domain_size)] + selector_col = Column("s", selector_vec, size=domain_size) + selector_col.interpolate(omega, prime) + selector_col.commit(pcs) + if selector_col.coeffs is None or selector_col.commitment is None: + raise ValueError("failed to build selector column") + return tuple(selector_col.evals), tuple(selector_col.coeffs), selector_col.commitment + + +# Assumption: Ring changes are infrequent, so caching the ring for a given set of keys is reasonable +@lru_cache(maxsize=8) +def _public_keys_column_data( + nm_points: tuple[tuple[int, int], ...], + domain_size: int, + omega: int, + prime: int, + pcs: Any, +) -> tuple[tuple[int, ...], tuple[int, ...], Any, tuple[int, ...], tuple[int, ...], Any]: + px_col = Column("Px", [point[0] for point in nm_points], size=domain_size) + py_col = Column("Py", [point[1] for point in nm_points], size=domain_size) + for col in (px_col, py_col): + col.interpolate(omega, prime) + col.commit(pcs) + if col.coeffs is None or col.commitment is None: + raise ValueError(f"failed to build {col.name} column") + return ( + tuple(px_col.evals), + tuple(px_col.coeffs), + px_col.commitment, + tuple(py_col.evals), + tuple(py_col.coeffs), + py_col.commitment, + ) diff --git a/dot_ring/vrf/ring/vrf.py b/dot_ring/vrf/ring/vrf.py index 6bdffb0..1ad4ea1 100644 --- a/dot_ring/vrf/ring/vrf.py +++ b/dot_ring/vrf/ring/vrf.py @@ -18,11 +18,11 @@ from dot_ring.ring_proof.proof_builder import RingProofBuilder from dot_ring.ring_proof.proof_payload import RingProofPayload from dot_ring.ring_proof.verify import RingProofFields, Verify, linear_pcs_verifications +from dot_ring.vrf.codec import point_len from dot_ring.vrf.pedersen import PedersenVRF from ..vrf import VRF from .members import Ring -from .members import parse_concatenated_keys as _parse_concatenated_keys from .root import RingRoot @@ -137,8 +137,8 @@ def _ring_proof_verifier( ring: Ring, ring_root: RingRoot, ) -> Verify: - fixed_cols_cmts = ring_root.fixed_commitments(ring.params) - transcript_prefix = ring_root.verifier_transcript_prefix(ring.params) + fixed_cols_cmts = ring_root.fixed_commitments() + transcript_prefix = ring_root.verifier_transcript_prefix() if isinstance(message, bytes): try: @@ -146,10 +146,12 @@ def _ring_proof_verifier( except ValueError as exc: raise ValueError("Invalid message point") from exc - seed_point = ring.params.cv.point_type(*ring.params.cv.curve.params.auxiliary_points.accumulator_base) + if not ring.params.cv.curve.params.auxiliary_points.accumulator_base: + raise ValueError("Curve does not have an accumulator base point for Ring VRF") + seed_point = ring.params.cv.point(ring.params.cv.curve.params.auxiliary_points.accumulator_base) rltn = message res_plus_seed = seed_point + rltn - witness_commitments, quotient_commitment = _proof_transcript_commitments(self, ring.params) + witness_commitments, quotient_commitment = self._proof_transcript_commitments(ring.params) return Verify( self.as_ring_proof(), @@ -164,7 +166,6 @@ def _ring_proof_verifier( prime=ring.params.prime, omega=ring.params.omega, pcs=ring.params.pcs, - domain_size_inv=pow(ring.params.domain_size, -1, ring.params.prime), transcript_witness_commitments=witness_commitments, transcript_quotient_commitment=quotient_commitment, ) @@ -217,7 +218,10 @@ def parse_keys(cls, keys: bytes) -> list[bytes]: Returns: List[bytes]: A list of individual keys extracted from the input bytes object. """ - return _parse_concatenated_keys(keys, cls.cv) + encoded_point_len = point_len(cls.cv) + if len(keys) % encoded_point_len != 0: + raise ValueError(f"invalid concatenated key length: expected multiple of {encoded_point_len}, got {len(keys)}") + return [keys[encoded_point_len * i : encoded_point_len * (i + 1)] for i in range(len(keys) // encoded_point_len)] def verify(self, input: bytes, ad_data: bytes, ring: Ring, ring_root: RingRoot) -> bool: """Spec section 5.2: Pedersen verify first, then ring-verify the blinded key.""" @@ -242,37 +246,24 @@ def batch_verify( ) -> bool: if not ring_root.matches_ring(ring): return False - if ring.params.cv.name != cls.cv.name: - return False - try: - pedersen_proofs = [] - for proof in proofs: - if proof.cv.name != cls.cv.name: - return False - pedersen_proofs.append(proof.pedersen_proof) - except (AttributeError, TypeError): + # Verify all Pedersen proofs first; if any fail, return immediately. + if not PedersenVRF[cls.cv].batch_verify([p.pedersen_proof for p in proofs], inputs, additional_data): return False - if not PedersenVRF[cls.cv].batch_verify(pedersen_proofs, inputs, additional_data): - return False - if not pedersen_proofs: - return True + fixed_cols_blst = tuple(g1_to_blst(commitment) for commitment in ring_root.fixed_commitments()) + transcript_prefix = ring_root.verifier_transcript_prefix() - fixed_cols_cmts = ring_root.fixed_commitments(ring.params) - fixed_cols_blst = tuple(g1_to_blst(commitment) for commitment in fixed_cols_cmts) - transcript_prefix = ring_root.verifier_transcript_prefix(ring.params) if not ring.params.cv.curve.params.auxiliary_points.accumulator_base: raise ValueError("Curve does not have an accumulator base point for Ring VRF") seed_point = ring.params.cv.point_type(*ring.params.cv.curve.params.auxiliary_points.accumulator_base) - domain = ring.params.domain - domain_size_inv = pow(ring.params.domain_size, -1, ring.params.prime) + pcs_verifications: list[Any] = [] try: for proof in proofs: relation = proof.pedersen_proof.blinded_pk result_plus_seed = seed_point + relation - witness_commitments, quotient_commitment = _proof_transcript_commitments(proof, ring.params) + witness_commitments, quotient_commitment = proof._proof_transcript_commitments(ring.params) pcs_verifications.extend( linear_pcs_verifications( proof.as_ring_proof(), @@ -280,12 +271,7 @@ def batch_verify( relation, result_plus_seed, seed_point, - domain, - ring.params.padding_rows, - domain_size_inv, - ring.params.cv.curve.params.a, - ring.params.omega, - ring.params.prime, + ring.params, transcript_prefix, witness_commitments, quotient_commitment, @@ -296,14 +282,13 @@ def batch_verify( return bool(ring.params.pcs.batch_verify_linear_preconverted(pcs_verifications)) - -def _proof_transcript_commitments(proof: RingVRF, params: RingProofParams) -> tuple[bytes, Any]: - commitments = ( - proof.c_b.commitment, - proof.c_accip.commitment, - proof.c_accx.commitment, - proof.c_accy.commitment, - proof.c_q.commitment, - ) - transcript_commitments = tuple(params.pcs.serialize_g1_uncompressed(commitment) for commitment in commitments) - return b"".join(transcript_commitments[:4]), transcript_commitments[4] + def _proof_transcript_commitments(self, params: RingProofParams) -> tuple[bytes, Any]: + commitments = ( + self.c_b.commitment, + self.c_accip.commitment, + self.c_accx.commitment, + self.c_accy.commitment, + self.c_q.commitment, + ) + transcript_commitments = tuple(params.pcs.serialize_g1_uncompressed(commitment) for commitment in commitments) + return b"".join(transcript_commitments[:4]), transcript_commitments[4] diff --git a/tests/benchmark/bench_ring_proof.py b/tests/benchmark/bench_ring_proof.py index b4c6949..26accf9 100644 --- a/tests/benchmark/bench_ring_proof.py +++ b/tests/benchmark/bench_ring_proof.py @@ -80,22 +80,28 @@ def _ring_keys(ring_size: int, sample_index: int, signer_key: bytes) -> list[byt def benchmark_once(ring_size: int, sample_index: int) -> Timing: public_key, secret_key = _keypair("signer", sample_index) keys = _ring_keys(ring_size, sample_index, public_key) - params = RingProofParams.from_ring_size(ring_size) alpha = b"bench-ring-input" + sample_index.to_bytes(8, "little") ad = b"bench-ring-ad" + sample_index.to_bytes(8, "little") total_start = time.perf_counter() start = time.perf_counter() - ring = Ring(keys, params) - ring_root = RingRoot.from_ring(ring, params) + ring = Ring.from_keys(keys) + ring_root = RingRoot.from_ring(ring) + root_bytes = ring_root.encode() ring_root_ms = (time.perf_counter() - start) * 1000 start = time.perf_counter() + ring = Ring.from_keys(keys) + ring_root = RingRoot.decode(root_bytes, ring) proof = RingVRF[Bandersnatch].prove(alpha, ad, secret_key, public_key, ring, ring_root) + proof_bytes = proof.encode() prove_ms = (time.perf_counter() - start) * 1000 start = time.perf_counter() + ring = Ring.from_keys(keys) + proof = RingVRF[Bandersnatch].decode(proof_bytes) + ring_root = RingRoot.decode(root_bytes, ring) verified = proof.verify(alpha, ad, ring, ring_root) verify_ms = (time.perf_counter() - start) * 1000 @@ -135,8 +141,8 @@ def _prove_batch_fixture_range(ring_size: int, start: int, stop: int) -> list[Se public_key, secret_key = _keypair("batch-signer", 0) keys = _ring_keys(ring_size, 0, public_key) params = RingProofParams.from_ring_size(ring_size) - ring = Ring(keys, params) - ring_root = RingRoot.from_ring(ring, params) + ring = Ring.from_keys(keys, params) + ring_root = RingRoot.from_ring(ring) fixtures: list[SerializedProofFixture] = [] for fixture_index in range(start, stop): @@ -159,25 +165,21 @@ def _chunk_ranges(count: int, workers: int) -> list[tuple[int, int]]: return ranges -def _build_batch_fixtures_parallel(ring_size: int, fixture_count: int, workers: int) -> tuple[Ring, RingRoot, list[ProofFixture]]: +def _build_batch_fixtures_parallel(ring_size: int, fixture_count: int, workers: int) -> tuple[Ring, RingRoot, list[ProofFixture], list[bytes]]: public_key, _ = _keypair("batch-signer", 0) keys = _ring_keys(ring_size, 0, public_key) - params = RingProofParams.from_ring_size(ring_size) - ring = Ring(keys, params) - ring_root = RingRoot.from_ring(ring, params) - - if workers <= 1: - serialized = _prove_batch_fixture_range(ring_size, 0, fixture_count) - else: - serialized = [] - with ProcessPoolExecutor(max_workers=workers) as executor: - futures = [executor.submit(_prove_batch_fixture_range, ring_size, start, stop) for start, stop in _chunk_ranges(fixture_count, workers)] - for future in futures: - serialized.extend(future.result()) + ring = Ring.from_keys(keys) + ring_root = RingRoot.from_ring(ring) + + serialized = [] + with ProcessPoolExecutor(max_workers=workers) as executor: + futures = [executor.submit(_prove_batch_fixture_range, ring_size, start, stop) for start, stop in _chunk_ranges(fixture_count, workers)] + for future in futures: + serialized.extend(future.result()) serialized.sort(key=lambda fixture: fixture.index) fixtures = [ProofFixture(RingVRF[Bandersnatch].decode(fixture.proof), fixture.alpha, fixture.ad) for fixture in serialized] - return ring, ring_root, fixtures + return ring, ring_root, fixtures, keys def run(ring_size: int, samples: int, max_batch_size: int, batch_fixtures: int | None, batch_workers: int) -> None: @@ -217,13 +219,16 @@ def run(ring_size: int, samples: int, max_batch_size: int, batch_fixtures: int | print(f"Batch verify: max_n={max_batch_size}, unique_fixtures={batch_fixtures}") print(f"Preparing proofs... | workers={workers}") - ring, ring_root, fixtures = _build_batch_fixtures_parallel(ring_size, batch_fixtures, workers) + ring, ring_root, fixtures, keys = _build_batch_fixtures_parallel(ring_size, batch_fixtures, workers) + ring_root_bytes = ring_root.encode() print() print("batch_n batch_ms batch_ms_per_proof") for batch_size in _batch_sizes(max_batch_size): batch = fixtures[:batch_size] start = time.perf_counter() + ring = Ring.from_keys(keys) + ring_root = RingRoot.decode(ring_root_bytes, ring) verified = RingVRF[Bandersnatch].batch_verify( [fixture.proof for fixture in batch], [fixture.alpha for fixture in batch], diff --git a/tests/test_bandersnatch_ark.py b/tests/test_bandersnatch_ark.py index 4c4a260..a9fa0be 100644 --- a/tests/test_bandersnatch_ark.py +++ b/tests/test_bandersnatch_ark.py @@ -53,7 +53,7 @@ def test_ietf_ark_bandersnatch(): assert proof_s.hex() == vector["proof_s"] if "beta" in vector: - assert TinyVRF[Bandersnatch].ecvrf_proof_to_hash(proof_bytes).hex() == vector["beta"] + assert TinyVRF[Bandersnatch].proof_to_hash(proof_rt.output_point).hex() == vector["beta"] assert proof.verify(pk_bytes, alpha, additional_data) assert proof_rt.encode() == proof_bytes @@ -98,10 +98,10 @@ def test_pedersen_ark_bandersnatch(): scalar_len(Bandersnatch), Bandersnatch.curve.encoding_endian(), ) == bytes.fromhex(vector["proof_sb"]) - assert PedersenVRF[Bandersnatch].ecvrf_proof_to_hash(proof.output_point.point_to_string()).hex() == vector["beta"] + assert PedersenVRF[Bandersnatch].proof_to_hash(proof.output_point).hex() == vector["beta"] if "beta" in vector: - assert PedersenVRF[Bandersnatch].ecvrf_proof_to_hash(proof.output_point.point_to_string()).hex() == vector["beta"] + assert PedersenVRF[Bandersnatch].proof_to_hash(proof.output_point).hex() == vector["beta"] assert proof.verify(alpha, additional_data) assert proof_rt.encode() == proof_bytes diff --git a/tests/test_curve_ops/test_gaps.py b/tests/test_curve_ops/test_gaps.py index 3d49538..068d279 100644 --- a/tests/test_curve_ops/test_gaps.py +++ b/tests/test_curve_ops/test_gaps.py @@ -8,72 +8,6 @@ class TestCoverageGaps: - def test_ecvrf_decode_proof_string_input(self): - """Test ecvrf_decode_proof handles hex string input.""" - # Generate a valid proof first - alpha = b"test" - sk = b"secret" - ad = b"ad" - proof = TinyVRF[Bandersnatch].prove(alpha, sk, ad) - proof_bytes = proof.encode() - proof_hex = proof_bytes.hex() - - gamma, c, s = TinyVRF[Bandersnatch].ecvrf_decode_proof(proof_hex) - assert gamma == proof.output_point - assert c == proof.c - assert s == proof.s - - def test_ecvrf_decode_proof_invalid_gamma(self): - """Test ecvrf_decode_proof raises ValueError for invalid gamma point.""" - invalid_gamma = b"\xff" * point_len(Bandersnatch) - c_len = Bandersnatch.curve.params.encoding.challenge_len - s_len = scalar_len(Bandersnatch) - dummy_c = b"\x00" * c_len - dummy_s = b"\x00" * s_len - - invalid_proof = invalid_gamma + dummy_c + dummy_s - - with pytest.raises(ValueError, match="Invalid gamma point"): - TinyVRF[Bandersnatch].ecvrf_decode_proof(invalid_proof) - - def test_ecvrf_decode_proof_invalid_s(self): - """Test ecvrf_decode_proof raises ValueError if S >= order.""" - alpha = b"test" - sk = b"secret" - ad = b"ad" - proof = TinyVRF[Bandersnatch].prove(alpha, sk, ad) - proof_bytes = proof.encode() - - # Modify S to be >= order - order = Bandersnatch.curve.params.subgroup_order - invalid_s = order + 1 - s_len = (order.bit_length() + 7) // 8 - - gamma_len = point_len(Bandersnatch) - c_len = Bandersnatch.curve.params.encoding.challenge_len - - gamma_bytes = proof_bytes[:gamma_len] - c_bytes = proof_bytes[gamma_len : gamma_len + c_len] - - invalid_s_bytes = invalid_s.to_bytes(s_len, "little") - - invalid_proof = gamma_bytes + c_bytes + invalid_s_bytes - - with pytest.raises(ValueError, match="scalar is not canonical"): - TinyVRF[Bandersnatch].ecvrf_decode_proof(invalid_proof) - - def test_ecvrf_proof_to_hash_string_input(self): - """Test ecvrf_proof_to_hash handles hex string input.""" - alpha = b"test" - sk = b"secret" - ad = b"ad" - proof = TinyVRF[Bandersnatch].prove(alpha, sk, ad) - proof_bytes = proof.encode() - proof_hex = proof_bytes.hex() - - hash_bytes = TinyVRF[Bandersnatch].ecvrf_proof_to_hash(proof_hex) - assert isinstance(hash_bytes, bytes) - def test_ietf_decode_invalid_point(self): """Test TinyVRF.decode raises ValueError for invalid output point.""" invalid_point = b"\xff" * point_len(Bandersnatch) @@ -133,26 +67,8 @@ def test_pedersen_decode_invalid_point(self): with pytest.raises(ValueError): # Message might vary depending on which point fails first PedersenVRF[Bandersnatch].decode(invalid_proof) - def test_pedersen_proof_to_hash_string(self): - """Test PedersenVRF.ecvrf_proof_to_hash handles hex string input.""" - alpha = b"test" - sk = b"secret" - ad = b"ad" - proof = PedersenVRF[Bandersnatch].prove(alpha, sk, ad) - proof_bytes = proof.output_point.point_to_string() - proof_hex = proof_bytes.hex() - - hash_bytes = PedersenVRF[Bandersnatch].ecvrf_proof_to_hash(proof_hex) - assert isinstance(hash_bytes, bytes) - - def test_pedersen_proof_to_hash_invalid(self): - """Test PedersenVRF.ecvrf_proof_to_hash raises ValueError for invalid point.""" - invalid_point = b"\xff" * 33 - with pytest.raises(ValueError, match="Invalid output point"): - PedersenVRF[Bandersnatch].ecvrf_proof_to_hash(invalid_point) - def test_uncompressed_curve_decode(self): - """Test ecvrf_decode_proof with uncompressed curve (Ed448).""" + """Test TinyVRF.decode with an uncompressed curve (Ed448).""" # Ed448 is uncompressed. alpha = b"test" sk = b"secret" @@ -162,9 +78,8 @@ def test_uncompressed_curve_decode(self): proof = TinyVRF[Ed448_RO].prove(alpha, sk, ad) proof_bytes = proof.encode() - # Decode - gamma, c, s = TinyVRF[Ed448_RO].ecvrf_decode_proof(proof_bytes) - assert gamma == proof.output_point + decoded = TinyVRF[Ed448_RO].decode(proof_bytes) + assert decoded.output_point == proof.output_point except Exception as e: pytest.skip(f"Ed448 not fully supported or failed: {e}") diff --git a/tests/test_ietf/test_ietf_ark.py b/tests/test_ietf/test_ietf_ark.py index 9c44c2a..439eeaf 100644 --- a/tests/test_ietf/test_ietf_ark.py +++ b/tests/test_ietf/test_ietf_ark.py @@ -73,7 +73,7 @@ def test_ietf_ark(curve_variant, file_prefix, subdir, gamma_len): assert int(proof_s.hex(), 16) == int(vector["proof_s"], 16) if "beta" in vector: - assert TinyVRF[curve_variant].ecvrf_proof_to_hash(proof_bytes).hex() == vector["beta"] + assert TinyVRF[curve_variant].proof_to_hash(proof_rt.output_point).hex() == vector["beta"] assert proof.verify(pk_bytes, alpha, additional_data, salt) assert proof_rt.encode() == proof_bytes diff --git a/tests/test_pedersen/test_pedersen_ark.py b/tests/test_pedersen/test_pedersen_ark.py index 65de4ee..daf9d4d 100644 --- a/tests/test_pedersen/test_pedersen_ark.py +++ b/tests/test_pedersen/test_pedersen_ark.py @@ -70,10 +70,10 @@ def test_pedersen_ietf(curve_variant, file_prefix, subdir): scalar_len(curve_variant), curve_variant.curve.encoding_endian(), ) == bytes.fromhex(vector["proof_sb"]) - assert PedersenVRF[curve_variant].ecvrf_proof_to_hash(proof.output_point.point_to_string()).hex() == vector["beta"] + assert PedersenVRF[curve_variant].proof_to_hash(proof.output_point).hex() == vector["beta"] if "beta" in vector: - assert PedersenVRF[curve_variant].ecvrf_proof_to_hash(proof.output_point.point_to_string()).hex() == vector["beta"] + assert PedersenVRF[curve_variant].proof_to_hash(proof.output_point).hex() == vector["beta"] assert proof.verify(alpha, additional_data) assert proof_rt.encode() == proof_bytes