Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 0 additions & 30 deletions .github/workflows/lint.yml

This file was deleted.

8 changes: 8 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ on:
- "*.*.*"

jobs:
test:
name: Run tests
uses: ./.github/workflows/test.yml
secrets: inherit

build_wheels_macos:
name: Build wheels on macos-latest
needs: test
runs-on: macos-latest

steps:
Expand Down Expand Up @@ -57,6 +63,7 @@ jobs:

build_wheels_linux:
name: Build wheels on ubuntu-latest
needs: test
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')

Expand Down Expand Up @@ -103,6 +110,7 @@ jobs:

build_sdist:
name: Build source distribution
needs: test
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
push:
branches: [ main ]
workflow_dispatch:
workflow_call:

jobs:
test:
Expand Down Expand Up @@ -33,6 +34,12 @@ jobs:
run: |
uv sync --extra dev

- name: Run ruff check
run: uv run --with ruff ruff check .

- name: Run ruff format check
run: uv run --with ruff ruff format --check .

- name: Setup environment (blst + cython)
run: |
uv run python scripts/setup_env.py
Expand All @@ -42,6 +49,7 @@ jobs:
PYTHONPATH: .
run: |
uv run pytest tests/ \
-n auto \
--cov=dot_ring \
--cov-report=term-missing \
--cov-report=xml \
Expand Down
47 changes: 26 additions & 21 deletions dot_ring/ring_proof/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ class RingProofParams:
test_vectors: bool = False
cv: ClassVar[CurveVariant] = Bandersnatch

@property
def scalar_bits(self) -> int:
return self.cv.curve.ORDER.bit_length()

@property
def row_overhead(self) -> int:
return self.scalar_bits + self.padding_rows

def __post_init__(self) -> None:
radix_domain_size = self.radix_domain_size
if radix_domain_size is None:
Expand All @@ -126,8 +134,14 @@ def __post_init__(self) -> None:
raise ValueError("padding_rows must be >= 1 to preserve accumulator structure")
if self.padding_rows >= self.domain_size:
raise ValueError("padding_rows must be less than domain_size")
if self.max_ring_size > self.domain_size - self.padding_rows:
raise ValueError(f"max_ring_size {self.max_ring_size} exceeds supported size {self.domain_size - self.padding_rows}")
max_supported = self.domain_size - self.row_overhead
if max_supported <= 0:
raise ValueError(
"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_supported:
raise ValueError(f"max_ring_size {self.max_ring_size} exceeds supported size {max_supported}")

@property
def omega(self) -> int:
Expand Down Expand Up @@ -157,7 +171,7 @@ def last_index(self) -> int:

@property
def max_effective_ring_size(self) -> int:
return self.domain_size - self.padding_rows
return self.domain_size - self.row_overhead

@classmethod
def from_ring_size(
Expand All @@ -172,8 +186,10 @@ def from_ring_size(
"""
Automatically construct RingProofParams based on ring size.

Calculates the minimum domain size needed to accommodate the ring
and constructs appropriate parameters.
The ring proof table needs one row per ring member, one row per scalar
bit in the producer index decomposition, and the fixed padding rows.
The returned max_ring_size is the full capacity for the selected
power-of-two domain, matching the ark-vrf sizing rule.

Args:
ring_size: Number of members in the ring
Expand All @@ -189,25 +205,14 @@ def from_ring_size(
raise ValueError(f"ring_size must be positive, got {ring_size}")

# Calculate minimum domain size needed:
# domain_size >= ring_size + padding_rows
min_domain_size = ring_size + padding_rows

# Round up to next power of 2
domain_size = _next_power_of_two(min_domain_size)

# Ensure domain_size is reasonable (between 16 and 8192)
if domain_size < 16:
domain_size = 16
elif domain_size > 8192:
raise ValueError(
f"Ring size {ring_size} requires domain size {domain_size}, "
f"which exceeds maximum supported size of 8192. "
f"Maximum ring size is {8192 - padding_rows}."
)
scalar_bits = cls.cv.curve.ORDER.bit_length()
overhead = scalar_bits + padding_rows
domain_size = _next_power_of_two(ring_size + overhead)
max_ring_size = domain_size - overhead

return cls(
domain_size=domain_size,
max_ring_size=ring_size,
max_ring_size=max_ring_size,
padding_rows=padding_rows,
prime=prime,
base_root=base_root,
Expand Down
8 changes: 5 additions & 3 deletions dot_ring/vrf/ring/ring_root.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ def __init__(self, keys: list[bytes], params: RingProofParams | None = None) ->

self.params = params

if len(keys) > params.domain_size - params.padding_rows:
raise ValueError(f"ring size {len(keys)} exceeds max supported size {params.domain_size - params.padding_rows}")
if len(keys) > params.max_ring_size:
raise ValueError(f"ring size {len(keys)} exceeds max supported size {params.max_ring_size}")

self.nm_points = []
for key in keys:
Expand Down Expand Up @@ -77,7 +77,9 @@ class RingRoot:
s: Column

@classmethod
def from_ring(cls, ring: Ring, params: RingProofParams):
def from_ring(cls, ring: Ring, params: RingProofParams | None = None):
if params is None:
params = ring.params
# Px, Py, s points
px, py = H.unzip(ring.nm_points)
selector_vec = [1 if i < params.max_ring_size else 0 for i in range(params.domain_size)]
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ dot_ring = [
dev = [
"pytest>=8.0.0",
"pytest-cov>=4.0.0",
"pytest-xdist>=3.6.0",
"pytest-benchmark>=4.0.0",
"Cython>=3.0.0",
"build>=1.0.0",
Expand Down Expand Up @@ -105,6 +106,8 @@ build-backend = "setuptools.build_meta"
dev = [
"cython>=3.2.1",
"pytest-benchmark>=5.2.3",
"pytest-cov>=7.0.0",
"pytest-xdist>=3.6.0",
"setuptools>=80.9.0",
]

Expand Down
20 changes: 11 additions & 9 deletions tests/test_coverage/test_columns.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import pytest

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.ring_root import Ring
from dot_ring.vrf.ring.ring_vrf import RingVRF


def test_column_interpolate_rejects_oversize_evals():
Expand All @@ -20,7 +22,7 @@ def test_column_commit_requires_coeffs():

def test_ring_from_params():
"""Test Ring construction with explicit params"""
params = RingProofParams(domain_size=8, max_ring_size=3, padding_rows=1)
params = RingProofParams(domain_size=512, max_ring_size=3, padding_rows=4)
# Create dummy keys (just using some bytes)
keys = [b"key1" * 8, b"key2" * 8]
ring = Ring(keys, params)
Expand All @@ -31,22 +33,22 @@ def test_ring_from_params():

def test_ring_rejects_oversize_ring():
"""Test Ring rejects rings that are too large"""
params = RingProofParams(domain_size=8, max_ring_size=2, padding_rows=1)
# Create 8 keys which exceeds max_ring_size=2 and domain_size-padding_rows=7
keys = [b"key" + bytes([i]) * 8 for i in range(8)]
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)]

with pytest.raises(ValueError, match="exceeds max supported size"):
Ring(keys, params)


def test_witness_builder_from_params_and_bits_vector_error():
params = RingProofParams(domain_size=8, max_ring_size=6, padding_rows=1)
builder = WitnessColumnBuilder.from_params(
ring_pk=[(0, 0)] * params.max_ring_size,
selector_vector=[0] * params.max_ring_size,
builder = WitnessColumnBuilder(
ring_pk=[(0, 0)] * 6,
selector_vector=[0] * 6,
producer_index=0,
secret_t=3,
params=params,
size=8,
max_ring_size=6,
padding_rows=1,
)
with pytest.raises(ValueError, match="b vector length exceeds"):
builder._bits_vector()
43 changes: 31 additions & 12 deletions tests/test_coverage/test_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,19 @@ def test_ring_proof_params_defaults():
assert params.radix_domain == list(D_2048)
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.padding_rows
assert params.max_effective_ring_size == params.domain_size - params.scalar_bits - params.padding_rows


def test_ring_proof_params_extends_root_size():
params = RingProofParams(
domain_size=2,
radix_domain_size=8,
prime=7,
base_root=1,
base_root_size=4,
padding_rows=1,
domain_size=512,
radix_domain_size=4096,
base_root_size=2048,
padding_rows=4,
max_ring_size=1,
)
assert params.base_root_size == 8
assert params.radix_domain_size == 8
assert params.base_root_size == 4096
assert params.radix_domain_size == 4096


@pytest.mark.parametrize(
Expand All @@ -104,11 +102,32 @@ def test_ring_proof_params_extends_root_size():
},
"must divide base_root_size",
),
({"domain_size": 8, "padding_rows": 0, "max_ring_size": 1}, "padding_rows must be >= 1"),
({"domain_size": 8, "padding_rows": 8, "max_ring_size": 1}, "padding_rows must be less than domain_size"),
({"domain_size": 8, "padding_rows": 2, "max_ring_size": 7}, "exceeds supported size"),
({"domain_size": 512, "padding_rows": 0, "max_ring_size": 1}, "padding_rows must be >= 1"),
({"domain_size": 512, "padding_rows": 512, "max_ring_size": 1}, "padding_rows must be less than domain_size"),
({"domain_size": 512, "padding_rows": 4, "max_ring_size": 256}, "exceeds supported size"),
({"domain_size": 256, "padding_rows": 4, "max_ring_size": 1}, "domain_size is too small"),
],
)
def test_ring_proof_params_validation_errors(kwargs, match):
with pytest.raises(ValueError, match=match):
RingProofParams(**kwargs)


@pytest.mark.parametrize(
("ring_size", "domain_size", "max_ring_size"),
[
(1, 512, 255),
(254, 512, 255),
(255, 512, 255),
(256, 1024, 767),
(767, 1024, 767),
(768, 2048, 1791),
(1791, 2048, 1791),
],
)
def test_from_ring_size_matches_spec_capacity(ring_size, domain_size, max_ring_size):
params = RingProofParams.from_ring_size(ring_size)

assert params.domain_size == domain_size
assert params.max_ring_size == max_ring_size
assert params.max_effective_ring_size == max_ring_size
Loading
Loading