From 0bce596370098f91ed3eebca70823b78e367edbe Mon Sep 17 00:00:00 2001 From: Vitalik Buterin Date: Sun, 12 Nov 2017 01:47:06 -0500 Subject: [PATCH] Added quadratic low degree testing --- zkstark/quadratic_prover_test.py | 8 ++ zkstark/quadratic_provers.py | 173 +++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 zkstark/quadratic_prover_test.py create mode 100644 zkstark/quadratic_provers.py diff --git a/zkstark/quadratic_prover_test.py b/zkstark/quadratic_prover_test.py new file mode 100644 index 0000000..e75d627 --- /dev/null +++ b/zkstark/quadratic_prover_test.py @@ -0,0 +1,8 @@ +import quadratic_provers as q + +data = q.eval_across_field([1, 2, 3, 4], 11) +qproof = q.mk_quadratic_proof(data, 4, 11) +assert q.check_quadratic_proof(data, qproof, 4, 5, 11) +data2 = q.eval_across_field(range(36), 97) +cproof = q.mk_column_proof(data2, 36, 97) +assert q.check_column_proof(data2, cproof, 36, 10, 97) diff --git a/zkstark/quadratic_provers.py b/zkstark/quadratic_provers.py new file mode 100644 index 0000000..4565c68 --- /dev/null +++ b/zkstark/quadratic_provers.py @@ -0,0 +1,173 @@ +# Evaluates a polynomial, expressed as an array where element i is the +# ith degree term, at coordinate x, in the prime field with the given +# modulus +def eval_poly_at(poly, x, modulus): + o = 0 + for p, v in enumerate(poly): + o += v * pow(x, p, modulus) + return o % modulus + +# Evaluates a polynomial for every coordinate in the given prime field +def eval_across_field(poly, modulus): + return [eval_poly_at(poly, i, modulus) for i in range(modulus)] + +# Interprets the given polynomial as a 2D polynomial (poly mod x^subdeg - y) +# and evaluates it at the given (x, y) coordinate +# eg. poly = [1, 2, 3, 4], subdeg = 2, the 2D polynomial becomes +# 4xy + 3y + 2x + 1 +def eval_2d_poly_at(poly, x, y, subdeg, modulus): + o = 0 + for p, v in enumerate(poly): + o += v * pow(x, p % subdeg, modulus) * pow(y, p // subdeg, modulus) + return o % modulus + +# Interprets the given polynomial as a 2D polynomial, and evaluates it +# across the entire field x field square +def eval_across_square(poly, max_x, max_y, subdeg, modulus): + o = [] + for y in range(max_y): + p = [] + for x in range(max_x): + p.append(eval_2d_poly_at(poly, x, y, subdeg, modulus)) + o.append(p) + return o + +# Recovers the polynomial that has the given y coordinates at the given +# x coordinates +def lagrange_interp(xs, ys, modulus): + # Generate master numerator polynomial + root = [1] + for i in range(len(xs)): + root.insert(0, 0) + for j in range(len(root)-1): + root[j] = (root[j] - root[j+1] * xs[i]) % modulus + # Generate per-value numerator polynomials by dividing the master + # polynomial back by each x coordinate + nums = [] + for i in range(len(xs)): + output = [] + last = 1 + for j in range(2,len(root)+1): + output.insert(0, last) + if j != len(root): + last = root[-j] + last * xs[i] + last = last % modulus + nums.append(output) + # Generate denominators by evaluating numerator polys at their x + denoms = [] + for i in range(len(xs)): + denom = 0 + xcpower = 1 + for j in range(len(nums[i])): + denom += xcpower * nums[i][j] + xcpower *= xs[i] + denoms.append(denom % modulus) + # Derive output + b = [0] * len(xs) + for i in range(len(xs)): + yslice = ys[i] * pow(denoms[i], modulus - 2, modulus) + for j in range(len(b)): + b[j] += nums[i][j] * yslice + return [x % modulus for x in b] + +# Is the number a perfect square? +def is_perf_square(n): return n ** 0.5 == int(n** 0.5) + +# Makes a low-degree proximity proof for the given polynomial +# by simply converting it into a 2D polynomial, with degree +# equal to the sqrt of the original degree - 1 and evaluating +# it at all (x, y) coordinates. The "diagonal" +# Q[i][i ** int(deg_lt ** 0.5)] for all i in the field is +# equivalent to the original data. +def mk_quadratic_proof(data, deg_lt, modulus): + # Derive the polynomial from the data + poly = lagrange_interp(range(len(data)), data, modulus) + # Check that the polynomial actually is low-degree + for i in range(deg_lt, len(poly)): + assert poly[i] == 0 + # Max degree must be a perfect square + assert is_perf_square(deg_lt) + # Evaluate it across the entire square + sq = eval_across_square(poly, modulus, modulus, int(deg_lt ** 0.5), modulus) + return sq + +# Checks the correctness of the above proof +def check_quadratic_proof(data, sq, deg_lt, checks, modulus): + import random + subdeg = int(deg_lt ** 0.5) + for _ in range(checks): + # Select a row and the corresponding column (column = row ** subdeg % modulus) + check_col = random.randrange(len(sq)) + check_row = pow(check_col, subdeg, modulus) + # Pick `subdeg` random cells in the same row + row_cells = [(col, sq[check_row][col]) for col in sorted(range(modulus), key=lambda x: random.random())[:subdeg]] + # Derive the polynomial (should be degree subdeg - 1) + row_poly = lagrange_interp([x[0] for x in row_cells], [x[1] for x in row_cells], modulus) + # Pick `subdeg` random cells in the same column + col_cells = [(row, sq[row][check_col]) for row in sorted(range(modulus), key=lambda x: random.random())[:subdeg]] + # Derive the polynomial (should be degree subdeg - 1) + col_poly = lagrange_interp([x[0] for x in col_cells], [x[1] for x in col_cells], modulus) + print('row %d eval' % check_row, eval_poly_at(row_poly, check_col, modulus)) + print('col %d eval' % check_col, eval_poly_at(col_poly, check_row, modulus)) + print('diag_in_sq', sq[check_row][check_col]) + print('in data', data[check_col]) + # Evaluate the polynomials along the "diagonal" (x, x ** subdeg), and check that the evaluated + # polynomials, the values in the square along the diagonal, and the original data all match + assert eval_poly_at(row_poly, check_col, modulus) == eval_poly_at(col_poly, check_row, modulus) == \ + sq[check_row][check_col] == data[check_col] + return True + +# Make a single-column low-degree proof +def mk_column_proof(data, deg_lt, modulus): + import random + # Derive the polynomial from the data + poly = lagrange_interp(range(len(data)), data, modulus) + # Check that the polynomial actually is low-degree + for i in range(deg_lt, len(poly)): + assert poly[i] == 0 + # Max degree must be a perfect square + assert is_perf_square(deg_lt) + # Max degree of the 2D polynomial (as in, the actual degree must be *less* than this) + subdeg = int(deg_lt ** 0.5) + # The order of the multiplicative group in the field (ie. p-1) must be a multiple + # of the subdeg, so that there are only (modulus - 1) // subdeg values of x ** subdeg + assert (modulus - 1) % subdeg == 0 + # All possible values of x ** subdeg + admissible_rows = [x for x in range(modulus) if pow(x, (modulus - 1) // subdeg, modulus) == 1] + assert len(admissible_rows) == modulus // subdeg + # Select a random x coordinate + xcor = random.randrange(modulus) + # Return a column consisting of the evaluations of the 2D polynomial at all admissible y + # coordinates + return (xcor, [eval_2d_poly_at(poly, xcor, row, subdeg, modulus) for row in admissible_rows]) + +# Check the above generated proof +def check_column_proof(data, proof, deg_lt, checks, modulus): + import random + subdeg = int(deg_lt ** 0.5) + check_col, column = proof + # All possible values of x ** subdeg + admissible_rows = [x for x in range(modulus) if pow(x, (modulus - 1) // subdeg, modulus) == 1] + for i in range(checks): + # Choose a random row to check + check_row = random.choice(admissible_rows) + print('Checking row %d' % check_row) + # Get the x coordinates that satisfy x ** subdeg % modulus == check_row. + # There are `subdeg` of these. + xs = [x for x in range(modulus) if pow(x, subdeg, modulus) == check_row] + print('Taking columns from data:', [(x, data[x]) for x in xs]) + if len(xs) != subdeg: + print('Row inadmissible; does not have %d roots' % subdeg) + continue + # Interpolate a degree subdeg-1 polynomial from the above + row_poly = lagrange_interp(xs, [data[x] for x in xs], modulus) + print('Eval', eval_poly_at(row_poly, check_col, modulus)) + print('Actual', column[admissible_rows.index(check_row)]) + # Evaluate the polynomial at the x coordinate of the column. Check that + # the value is the same as the value provided + assert eval_poly_at(row_poly, check_col, modulus) == column[admissible_rows.index(check_row)] + # Check that the column itself is low degree + column_poly = lagrange_interp(admissible_rows, column, modulus) + for i in range(subdeg+1, len(column_poly)): + assert column_poly[i] == 0 + return True