Skip to content

Commit e0c3bcb

Browse files
Merge pull request #27 from Chainscore/feat/zk-row-blinding
Add ZK-row random blinding to ring proof witness columns
2 parents 8334135 + 2adf2db commit e0c3bcb

6 files changed

Lines changed: 72 additions & 13 deletions

File tree

dot_ring/ring_proof/columns/columns.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
import json
44
import os
5+
import secrets
56
from dataclasses import dataclass
67
from typing import cast
78

8-
from dot_ring.ring_proof.constants import DEFAULT_SIZE, MAX_RING_SIZE, OMEGAS, S_PRIME, SeedPoint
9+
from dot_ring.ring_proof.constants import DEFAULT_SIZE, MAX_RING_SIZE, OMEGAS, S_PRIME, ZK_ROWS, SeedPoint
910
from dot_ring.ring_proof.curve.bandersnatch import TwistedEdwardCurve as TE
1011
from dot_ring.ring_proof.helpers import Helpers as H
1112
from dot_ring.ring_proof.params import RingProofParams
@@ -27,12 +28,30 @@ class Column:
2728
commitment: G1Point | None = None
2829
size: int = DEFAULT_SIZE
2930

30-
def interpolate(self, domain_omega: int = OMEGAS[DEFAULT_SIZE], prime: int = S_PRIME) -> None:
31-
"""Fill `self.coeffs` from `self.evals` using FFT interpolation."""
31+
def interpolate(
32+
self,
33+
domain_omega: int = OMEGAS[DEFAULT_SIZE],
34+
prime: int = S_PRIME,
35+
hidden: bool = False,
36+
test_vectors: bool = False,
37+
) -> None:
38+
"""Fill `self.coeffs` from `self.evals` using FFT interpolation.
39+
40+
When ``hidden=True`` and ``test_vectors=False``, the last
41+
``ZK_ROWS`` positions are filled with cryptographically random
42+
field elements (random blinding) to preserve zero-knowledge.
43+
"""
3244
if self.coeffs is None:
33-
if len(self.evals) > self.size:
34-
raise ValueError(f"{self.name} evals length {len(self.evals)} exceeds column size {self.size}")
35-
self.evals += [0] * (self.size - len(self.evals))
45+
if hidden and not test_vectors:
46+
capacity = self.size - ZK_ROWS
47+
if len(self.evals) > capacity:
48+
raise ValueError(f"{self.name} evals length {len(self.evals)} exceeds capacity {capacity} (size={self.size}, ZK_ROWS={ZK_ROWS})")
49+
self.evals += [0] * (capacity - len(self.evals))
50+
self.evals += [secrets.randbelow(prime) for _ in range(ZK_ROWS)]
51+
else:
52+
if len(self.evals) > self.size:
53+
raise ValueError(f"{self.name} evals length {len(self.evals)} exceeds column size {self.size}")
54+
self.evals += [0] * (self.size - len(self.evals))
3655
self.coeffs = poly_interpolate_fft(self.evals, domain_omega, prime)
3756

3857
def commit(self) -> None:
@@ -53,6 +72,7 @@ class WitnessColumnBuilder:
5372
prime: int = S_PRIME
5473
max_ring_size: int = MAX_RING_SIZE
5574
padding_rows: int = 4
75+
test_vectors: bool = False
5676

5777
@classmethod
5878
def from_params(
@@ -73,6 +93,7 @@ def from_params(
7393
prime=params.prime,
7494
max_ring_size=params.max_ring_size,
7595
padding_rows=params.padding_rows,
96+
test_vectors=params.test_vectors,
7697
)
7798

7899
def _bits_vector(self) -> list[int]:
@@ -120,7 +141,7 @@ def build(self) -> tuple[Column, Column, Column, Column]:
120141
Column("accip", acc_ip, size=self.size),
121142
]
122143
for col in columns:
123-
col.interpolate(self.omega, self.prime)
144+
col.interpolate(self.omega, self.prime, hidden=True, test_vectors=self.test_vectors)
124145
col.commit()
125146
return (columns[0], columns[1], columns[2], columns[3])
126147

dot_ring/ring_proof/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262

6363
MAX_RING_SIZE: int = 255 # Upper bound enforced by the constraint system
6464

65+
ZK_ROWS: int = 3 # Number of random blinding rows for zero-knowledge (matches Rust ZK_ROWS)
66+
6567

6668
__all__ = [
6769
"S_PRIME",
@@ -78,4 +80,5 @@
7880
"D_512",
7981
"D_2048",
8082
"MAX_RING_SIZE",
83+
"ZK_ROWS",
8184
]

dot_ring/ring_proof/params.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ class RingProofParams:
102102
prime: int = S_PRIME
103103
base_root: int = OMEGA_2048
104104
base_root_size: int = 2048
105+
test_vectors: bool = False
105106
cv: ClassVar[CurveVariant] = Bandersnatch
106107

107108
def __post_init__(self) -> None:
@@ -166,6 +167,7 @@ def from_ring_size(
166167
prime: int = S_PRIME,
167168
base_root: int = OMEGA_2048,
168169
base_root_size: int = 2048,
170+
test_vectors: bool = False,
169171
) -> RingProofParams:
170172
"""
171173
Automatically construct RingProofParams based on ring size.
@@ -210,4 +212,5 @@ def from_ring_size(
210212
prime=prime,
211213
base_root=base_root,
212214
base_root_size=base_root_size,
215+
test_vectors=test_vectors,
213216
)

tests/test_bandersnatch_ark.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def test_ring_proof():
121121
ad = bytes.fromhex(item["ad"])
122122
keys = RingVRF[Bandersnatch].parse_keys(bytes.fromhex(item["ring_pks"]))
123123
start = time()
124-
params = RingProofParams()
124+
params = RingProofParams(test_vectors=True)
125125
ring = Ring(keys, params)
126126
ring_root = RingRoot.from_ring(ring, params)
127127
ring_time = time()

tests/test_ring_vrf/test_ring_vrf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def test_ring_proof():
2424
keys = RingVRF[Bandersnatch].parse_keys(bytes.fromhex(item["ring_pks"]))
2525

2626
start_time = time.time()
27-
params = RingProofParams()
27+
params = RingProofParams(test_vectors=True)
2828
ring = Ring(keys, params)
2929
ring_root = RingRoot.from_ring(ring, params)
3030
ring_time = time.time()

tests/test_vectors.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,8 @@ def verify_ring_vector(vector: dict[str, Any], curve) -> None:
271271
for i in range(0, len(ring_pks_bytes), point_len):
272272
ring_pks.append(ring_pks_bytes[i : i + point_len])
273273

274-
# Construct ring and ring root
275-
params = RingProofParams()
274+
# Construct ring and ring root (test_vectors=True for deterministic proofs)
275+
params = RingProofParams(test_vectors=True)
276276
ring = Ring(ring_pks, params)
277277
ring_root = RingRoot.from_ring(ring, params)
278278

@@ -417,8 +417,8 @@ def test_wrong_ring_root(self):
417417
alpha = b"test_input"
418418
ad = b"test_ad"
419419

420-
# Construct rings and ring roots
421-
params = RingProofParams()
420+
# Construct rings and ring roots (test_vectors=True for deterministic proofs)
421+
params = RingProofParams(test_vectors=True)
422422
ring_obj1 = Ring(ring1, params)
423423
ring_root1 = RingRoot.from_ring(ring_obj1, params)
424424
ring_obj2 = Ring(ring2, params)
@@ -470,3 +470,35 @@ def test_pedersen_deterministic(self):
470470
assert proof1.ok.point_to_string() == proof2.ok.point_to_string()
471471
assert proof1.s == proof2.s
472472
assert proof1.sb == proof2.sb
473+
474+
def test_ring_nondeterministic(self):
475+
"""Ring VRF proofs with default params (test_vectors=False) should be non-deterministic.
476+
477+
Two proofs from the same inputs should differ due to random ZK-row blinding,
478+
but both must still verify correctly.
479+
"""
480+
sk = bytes.fromhex("0101010101010101010101010101010101010101010101010101010101010101")
481+
pk = RingVRF[Bandersnatch].get_public_key(sk)
482+
483+
ring_keys = [pk]
484+
for i in range(7):
485+
other_sk = (i + 2).to_bytes(32, "little")
486+
ring_keys.append(RingVRF[Bandersnatch].get_public_key(other_sk))
487+
488+
alpha = b"deterministic_test"
489+
ad = b"test_ad"
490+
491+
# Default params: test_vectors=False → random ZK-row blinding
492+
params = RingProofParams(test_vectors=False)
493+
ring = Ring(ring_keys, params)
494+
ring_root = RingRoot.from_ring(ring, params)
495+
496+
proof1 = RingVRF[Bandersnatch].prove(alpha, ad, sk, pk, ring, ring_root)
497+
proof2 = RingVRF[Bandersnatch].prove(alpha, ad, sk, pk, ring, ring_root)
498+
499+
# Proof bytes should differ due to random blinding
500+
assert proof1.to_bytes() != proof2.to_bytes(), "Ring proofs should be non-deterministic with random ZK-row blinding"
501+
502+
# Both proofs must still verify
503+
assert proof1.verify(alpha, ad, ring, ring_root), "First proof verification failed"
504+
assert proof2.verify(alpha, ad, ring, ring_root), "Second proof verification failed"

0 commit comments

Comments
 (0)