├── local ├── __init__.py ├── ec │ ├── __init__.py │ ├── util.py │ ├── static.py │ └── core.py ├── graph.py ├── primes.py ├── exact_cover.py └── sudoku.py ├── requirements.txt ├── double_pendulum.gif ├── plot_curve_real.py ├── chapter_dependencies.dot ├── plot_curve_finite.py ├── flake.nix ├── generate_lookup_tables.py ├── generate_chi_square.py ├── flake.lock ├── generate_curve.sage ├── meta.py ├── customization.md ├── hardness_dlog.py ├── cyclic_lagrange.py ├── .gitignore ├── expectations.md ├── README.md ├── LICENSE ├── chapter_dependencies.svg ├── basis └── commitments.ipynb ├── play_sudoku.py ├── graph ├── non_isomorphism.ipynb ├── isomorphism.ipynb └── three_coloring.ipynb └── easy └── schnorr.ipynb /local/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /local/ec/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib 2 | numpy 3 | notebook 4 | networkx 5 | ipywidgets 6 | pygame 7 | -------------------------------------------------------------------------------- /double_pendulum.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uncomputable/zkp-workshop/HEAD/double_pendulum.gif -------------------------------------------------------------------------------- /plot_curve_real.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from local.ec.core import PARAMETER_A, PARAMETER_B 4 | 5 | 6 | def plot(): 7 | # Initialize the plot 8 | fig, ax = plt.subplots() 9 | ax.set_title(f"Elliptic curve $y^2 = x^3 + {PARAMETER_A.value}x + {PARAMETER_B.value}$ over ℝ") 10 | ax.set_xlabel("x") 11 | ax.set_ylabel("y") 12 | plt.grid(True) 13 | 14 | # Compute points on the curve 15 | x = np.linspace(-5, 5, 100) 16 | y = np.linspace(-5, 5, 100) 17 | x, y = np.meshgrid(x, y) 18 | z = pow(y, 2) - pow(x, 3) - x * PARAMETER_A.value - PARAMETER_B.value 19 | 20 | # Plot the curve 21 | plt.contour(x, y, z, [0]) 22 | 23 | # Show plot 24 | plt.show() 25 | 26 | 27 | if __name__ == "__main__": 28 | plot() 29 | -------------------------------------------------------------------------------- /chapter_dependencies.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | ranksep=3; 3 | 4 | // Legend 5 | subgraph legend { 6 | a [style=invis]; 7 | b [style=invis]; 8 | c [style=invis]; 9 | d [style=invis]; 10 | a -> b [label="Chapter dependency", color="darkgray"]; 11 | c -> d [label="Recommended path", color="blue"]; 12 | } 13 | 14 | // Chapter dependencies 15 | edge[color="darkgray"] 16 | 17 | "Basis/Interactive Proofs" -> { 18 | "Easy/Schnorr" 19 | "Basis/Commitments" 20 | "Graph/Nonisomorphism" 21 | "Graph/Isomorphism" 22 | }; 23 | "Basis/Commitments" -> { 24 | "Graph/Colorability" 25 | "Game/Sudoku" 26 | }; 27 | "Basis/Elliptic Curves" -> { 28 | "Easy/Schnorr" 29 | "Basis/Commitments" 30 | }; 31 | 32 | // Recommended path 33 | edge[color="blue", constraint=false] 34 | 35 | "Basis/Interactive Proofs" 36 | -> "Basis/Elliptic Curves" 37 | -> "Easy/Schnorr" 38 | -> "Graph/Nonisomorphism" 39 | -> "Basis/Commitments" 40 | -> "Game/Sudoku"; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /plot_curve_finite.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from local.ec.static import XY 4 | from local.ec.core import MAX_COORDINATE, PARAMETER_A, PARAMETER_B 5 | 6 | 7 | def plot(): 8 | # Initialize the plot 9 | fig, ax = plt.subplots() 10 | ax.set_title("Elliptic curve $y^2 \equiv x^3 + {}x + {}$ (mod {})".format(PARAMETER_A.value, PARAMETER_B.value, MAX_COORDINATE)) 11 | ax.set_xlabel("x") 12 | ax.set_ylabel("y") 13 | 14 | # Initialize an array of zeros (white squares) 15 | points = np.zeros((MAX_COORDINATE, MAX_COORDINATE)) 16 | 17 | # Add points on the curve 18 | for (dlog, xy) in enumerate(XY): 19 | if xy: 20 | # Inverted coordinates 21 | # because imshow treats first component as rows (y coordinate) 22 | # and second component as columns (x coordinate) 23 | points[xy[1], xy[0]] = 1 24 | ax.text(xy[0], xy[1], str(dlog), ha="center", va="center", color="white") 25 | 26 | # Plot the curve 27 | # Set origin to the bottom left 28 | # because imshow places it at the top left by default 29 | ax.imshow(points, cmap='gray_r', origin='lower') 30 | 31 | # Show plot 32 | plt.show() 33 | 34 | 35 | if __name__ == "__main__": 36 | plot() 37 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "ZKP Workshop"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { 10 | self, 11 | nixpkgs, 12 | flake-utils 13 | }: 14 | flake-utils.lib.eachDefaultSystem (system: 15 | let 16 | pkgs = nixpkgs.legacyPackages.${system}; 17 | python = pkgs.python3; 18 | coreDeps = with python.pkgs; [ 19 | matplotlib 20 | numpy 21 | notebook 22 | networkx 23 | ipywidgets 24 | pygame 25 | ]; 26 | scipyDeps = with python.pkgs; coreDeps ++ [ 27 | scipy 28 | ]; 29 | corePython = python.withPackages (_: coreDeps); 30 | scipyPython = python.withPackages (_: scipyDeps); 31 | in 32 | { 33 | devShells = { 34 | default = pkgs.mkShell { 35 | packages = [ corePython ]; 36 | }; 37 | scipy = pkgs.mkShell { 38 | packages = [ scipyPython ]; 39 | }; 40 | sage = pkgs.mkShell { 41 | # Sage is not available on macOS 42 | packages = pkgs.lib.optionals (!pkgs.stdenv.isDarwin) [ pkgs.sage ]; 43 | }; 44 | }; 45 | packages = { 46 | default = pkgs.writeShellScriptBin "jupyter" '' 47 | ${corePython}/bin/python -m notebook 48 | ''; 49 | }; 50 | } 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /generate_lookup_tables.py: -------------------------------------------------------------------------------- 1 | """ 2 | Use this script to generate the constants in local.ec.static.py and hardness_dlog.py. 3 | """ 4 | 5 | from local.ec.core import ONE_POINT, ZERO_POINT, NUMBER_POINTS, MAX_COORDINATE, PARAMETER_A, PARAMETER_B 6 | from typing import List, Optional, Tuple 7 | import meta 8 | import os 9 | 10 | IntPoint = Optional[Tuple[int, int]] 11 | 12 | 13 | def point_xy() -> List[IntPoint]: 14 | current = ZERO_POINT 15 | xy = [] 16 | 17 | while True: 18 | if current.is_zero(): 19 | xy.append(None) 20 | else: 21 | xy.append((current.x.value, current.y.value)) 22 | current += ONE_POINT 23 | 24 | if current == ZERO_POINT: 25 | break 26 | 27 | return xy 28 | 29 | 30 | xy = "({})".format(", ".join(["{}".format(xy) for xy in point_xy()])) 31 | 32 | patterns = ( 33 | lambda x: f"MAX_COORDINATE = {x}", 34 | lambda x: f"NUMBER_POINTS = {x}", 35 | lambda x: f"XY = {x}", 36 | ) 37 | updated_values = (MAX_COORDINATE, NUMBER_POINTS, xy) 38 | 39 | meta.update_variables(os.path.join("local", "ec", "static.py"), patterns, updated_values) 40 | 41 | patterns = ( 42 | lambda x: f"MAX_COORDINATE = {x}", 43 | lambda x: f"PARAMETER_A = {x}", 44 | lambda x: f"PARAMETER_B = {x}", 45 | lambda x: f"NUMBER_POINTS = {x}", 46 | lambda x: f"XY = {x}", 47 | ) 48 | updated_values = (MAX_COORDINATE, PARAMETER_A.value, PARAMETER_B.value, NUMBER_POINTS, xy) 49 | 50 | meta.update_variables("hardness_dlog.py", patterns, updated_values) 51 | -------------------------------------------------------------------------------- /generate_chi_square.py: -------------------------------------------------------------------------------- 1 | """ 2 | Use this script to generate critical chi-square values. 3 | 4 | For each number of degrees of freedom, there is a critical value. 5 | Therefore, the output is a list of chi-square values. 6 | These values additionally depend on the so-called significance level. 7 | You can adjust both parameters below; then run the main method. 8 | 9 | You need scipy to run this script! 10 | """ 11 | 12 | from scipy.stats import chi2 13 | import meta 14 | import os 15 | 16 | degrees_freedom = range(1, 10000) 17 | """ 18 | Range of degrees of freedom. 19 | Distributions with many different values (bins) have many degrees of freedom. 20 | 21 | From 1 to any positive integer. 22 | """ 23 | 24 | significance = 0.05 25 | """ 26 | Significance level. 27 | Probability of rejecting the null hypothesis when it is in fact true (false negative). 28 | 29 | A lower significance level means you are conservative 30 | and require more evidence before rejecting the null hypothesis. 31 | This increases the number of true positives and of false positives. 32 | 33 | A higher significance level means you are lenient 34 | and require less evidence before rejecting the null hypothesis. 35 | This increases the number true negatives and of false negatives. 36 | 37 | From 0 to 1. 38 | """ 39 | 40 | chi_squared_values = [chi2.ppf(1 - significance, df) for df in degrees_freedom] 41 | chi_squared_fmt = "({})".format(", ".join(["{:0.2f}".format(x) for x in chi_squared_values])) 42 | 43 | pattern = lambda x: f"CRITICAL_CHI_SQUARE_VALUES = {x}" 44 | updated_value = chi_squared_fmt 45 | 46 | meta.update_variable(os.path.join("local", "stats.py"), pattern, updated_value) 47 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1694529238, 9 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1696375444, 24 | "narHash": "sha256-Sv0ICt/pXfpnFhTGYTsX6lUr1SljnuXWejYTI2ZqHa4=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "81e8f48ebdecf07aab321182011b067aafc78896", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /generate_curve.sage: -------------------------------------------------------------------------------- 1 | """ 2 | Use this script to generate small elliptic curves. 3 | 4 | An elliptic curve is defined as y^2 = x^3 + a * x + b (mod p) 5 | where a, b, p are parameters. 6 | Any point (x, y) that satisfies this equation is on the curve. 7 | 8 | a and b are coefficients and p is the field modulus. 9 | 10 | There is also the number of points on a curve, denoted n. 11 | We can calculate this for every curve and it should be prime, 12 | so that our group has the desired arithmetic properties (cyclic, etc.). 13 | 14 | This script keeps a = 0. This simplifies our equations and 15 | enables an optimized implementation of EC operations. 16 | secp256k1 uses the same trick. 17 | 18 | You need sage(math) to run this script! 19 | """ 20 | from typing import Tuple 21 | import meta 22 | import os 23 | 24 | def find_curve(p: int) -> Tuple[int, int, int, int]: 25 | for _ in range(0, 10000): 26 | p = next_prime(p) 27 | F = FiniteField(p) 28 | a = 0 29 | 30 | for b in range(1, 20): 31 | try: 32 | C = EllipticCurve([F(a), F(b)]) 33 | except ArithmeticError: 34 | continue 35 | n = C.order() 36 | 37 | if n.is_prime(): 38 | return p, a, b, n 39 | 40 | raise StopIteration 41 | 42 | 43 | p = input("Initial field modulus (positive integer): ") 44 | 45 | try: 46 | p = int(p) 47 | p, a, b, n = find_curve(p) 48 | 49 | patterns = ( 50 | lambda x: f"MAX_COORDINATE = {x}", 51 | lambda x: f"PARAMETER_A = Coordinate({x})", 52 | lambda x: f"PARAMETER_B = Coordinate({x})", 53 | lambda x: f"NUMBER_POINTS = {x}" 54 | ) 55 | updated_values = (p, a, b, n) 56 | 57 | meta.update_variables(os.path.join("local", "ec", "core.py"), patterns, updated_values) 58 | except ValueError: 59 | print("Field modulus must be an integer") 60 | except StopIteration: 61 | print("Could not find any curves within the given search space") 62 | -------------------------------------------------------------------------------- /meta.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, TypeVar, Tuple 2 | import os 3 | import re 4 | import sys 5 | 6 | T = TypeVar("T") 7 | 8 | 9 | def update_variables(file_path: str, patterns: Tuple[Callable[[T], str], ...], updated_values: Tuple[T, ...]): 10 | """ 11 | Updates the variables inside the given file to the given updated values. 12 | 13 | **Each variable should occur exactly once in the file!** 14 | 15 | :param file_path: Python file 16 | :param patterns: For each variable, function that takes a value and returns the string of the form `name = value` that defines the variable 17 | :param updated_values: For each variable, new value for the variable 18 | """ 19 | if not os.path.exists(file_path): 20 | raise ValueError(f"{file_path} does not exist. Run this script from the root directory of this repo.") 21 | if len(patterns) != len(updated_values): 22 | raise ValueError("Need as many patterns as updated values") 23 | 24 | updates = [patterns[i](updated_values[i]) for i in range(len(patterns))] 25 | apply = input(f"Do you want to apply the update to {file_path}? [y,n,q]: ").lower() 26 | 27 | if apply == "y": 28 | with open(file_path, "r") as f: 29 | file_data = f.read() 30 | 31 | for i in range(len(patterns)): 32 | regex = re.compile(re.escape(patterns[i]("できれば現れない文字")).replace("できれば現れない文字", ".*")) 33 | 34 | if not regex.search(file_data): 35 | print(f"Variable not found in {file_path}. Wrong file name?") 36 | 37 | file_data = regex.sub(updates[i], file_data) 38 | 39 | with open(file_path, "w") as f: 40 | f.write(file_data) 41 | 42 | print(f"Successfully updated {file_path}") 43 | elif apply == "q": 44 | sys.exit(0) 45 | else: 46 | do_print = input("Print updated values? [y,n,q]: ") 47 | if do_print.lower() == "y": 48 | for update in updates: 49 | print(update) 50 | 51 | print("No file update") 52 | 53 | 54 | def update_variable(file_path: str, pattern: Callable[[T], str], updated_value: T): 55 | """ 56 | Updates the variable inside the given file to the given updated value. 57 | 58 | **The variable should occur exactly once in the file!** 59 | 60 | :param file_path: Python file 61 | :param pattern: Function that takes a value and returns the string of the form `name = value` that defines the variable 62 | :param updated_value: New value for the variable 63 | """ 64 | update_variables(file_path, (pattern,), (updated_value,)) 65 | -------------------------------------------------------------------------------- /customization.md: -------------------------------------------------------------------------------- 1 | # Customization 2 | 3 | You can change the parameters used in this workshop and recompute the hardcoded values. 4 | 5 | Some of these functions require additional dependencies to work. 6 | 7 | ## Run with Scipy 8 | 9 | Scipy can be installed via nix 10 | 11 | ``` 12 | nix develop .#scipy 13 | ``` 14 | 15 | or via pip 16 | 17 | ``` 18 | pip install scipy 19 | ``` 20 | 21 | ## Generate critical chi-square values (requires Scipy) 22 | 23 | The chi-square test uses a significance which is implicitly encoded in the critical chi-square values. The default significance is the usual 5%. 24 | 25 | There is one chi-square value for each number of degrees of freedom. We generated values up to a maximum number. 26 | 27 | Edit [this script](https://github.com/uncomputable/zkp-workshop/blob/master/generate_chi_square.py) to change these parameters. 28 | 29 | Run the script on the command line. 30 | 31 | ``` 32 | python3 generate_chi_square.py 33 | ``` 34 | 35 | It will directly overwrite the Python files inside the repository after asking you for confirmation. 36 | 37 | ## Generate EC lookup tables 38 | 39 | Most elliptic curve operations use static lookup tables. It is much easier to treat curve points as literal integers instead of 2D points with arithmetic properties. The catch is that we need to know the discrete logarithm. It works for us because we use small curves. 40 | 41 | In real life, the curves would be too large to make lookups feasible. The lookup table would be larger than the number of atoms in the universe. Keep this in mind. 42 | 43 | Run this script to generate the static lookup tables. 44 | 45 | This is especially useful after you changed the curve we work on (see below). 46 | 47 | ``` 48 | python3 generate_lookup_tables.py 49 | ``` 50 | 51 | It will directly overwrite the Python files inside the repository after asking you for confirmation. 52 | 53 | ## Run with Sage 54 | 55 | Oh, no. You almost never want to run Sage :O 56 | 57 | You can install Sage easily via nix. 58 | 59 | ``` 60 | nix develop .#sage 61 | ``` 62 | 63 | If you use pip and the package manager, I guess you can use the [official instructions](https://doc.sagemath.org/html/en/installation/index.html). Good luck :) 64 | 65 | ## Change the curve (requires Sage) 66 | 67 | The parameters of the elliptic curve we use are defined in [the core EC implementation](https://github.com/uncomputable/zkp-workshop/blob/master/local/ec/core.py). 68 | 69 | This is a small elliptic curve which is similar to [secp256k1](https://en.bitcoin.it/wiki/Secp256k1). 70 | 71 | Edit [this script](https://github.com/uncomputable/zkp-workshop/blob/master/generate_curve.sage) to change these parameters. 72 | 73 | Run the script on the command line. 74 | 75 | ``` 76 | sage generate_curve.sage 77 | ``` 78 | 79 | It will directly overwrite the Python files inside the repository after asking you for confirmation. 80 | 81 | Make sure to update the static lookup tables (see above). 82 | -------------------------------------------------------------------------------- /hardness_dlog.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from matplotlib.widgets import Slider, Button 4 | 5 | MAX_COORDINATE = 103 6 | PARAMETER_A = 0 7 | PARAMETER_B = 5 8 | NUMBER_POINTS = 97 9 | XY = (None, (94, 93), (67, 27), (23, 92), (3, 49), (8, 38), (30, 15), (86, 97), (52, 92), (68, 94), (60, 80), (28, 11), (6, 85), (66, 50), (59, 38), (87, 21), (32, 15), (49, 50), (19, 13), (36, 65), (33, 43), (97, 43), (41, 88), (2, 42), (100, 94), (95, 27), (47, 101), (44, 76), (82, 80), (9, 42), (91, 53), (50, 13), (27, 85), (38, 9), (102, 101), (11, 93), (101, 10), (57, 101), (64, 23), (42, 97), (31, 21), (76, 60), (88, 21), (65, 49), (70, 18), (92, 42), (78, 6), (35, 54), (34, 13), (34, 90), (35, 49), (78, 97), (92, 61), (70, 85), (65, 54), (88, 82), (76, 43), (31, 82), (42, 6), (64, 80), (57, 2), (101, 93), (11, 10), (102, 2), (38, 94), (27, 18), (50, 90), (91, 50), (9, 61), (82, 23), (44, 27), (47, 2), (95, 76), (100, 9), (2, 61), (41, 15), (97, 60), (33, 60), (36, 38), (19, 90), (49, 53), (32, 88), (87, 82), (59, 65), (66, 53), (6, 18), (28, 92), (60, 23), (68, 9), (52, 11), (86, 6), (30, 88), (8, 65), (3, 54), (23, 11), (67, 76), (94, 10)) 10 | 11 | # Initialize plot 12 | fig, ax = plt.subplots() 13 | ax.set_title("Elliptic curve $y^2 \equiv x^3 + {}x + {}$ (mod {})".format(PARAMETER_A, PARAMETER_B, MAX_COORDINATE)) 14 | ax.set_xlabel("x") 15 | ax.set_ylabel("y") 16 | 17 | # Initialize an array of zeros (white squares) 18 | points = np.zeros((MAX_COORDINATE, MAX_COORDINATE)) 19 | 20 | # Draw first point 21 | x, y = XY[1] 22 | points[y, x] = 1 23 | im = ax.imshow(points, cmap='gray_r', origin='lower', animated=True) 24 | 25 | # Iteration slider 26 | ax_slider = fig.add_axes([0.1, 0.1, 0.05, 0.75]) # [left, bottom, width, height] 27 | slider = Slider( 28 | ax=ax_slider, 29 | label="Point number", 30 | valmin=0, 31 | valmax=NUMBER_POINTS - 1, 32 | valstep=1, 33 | valinit=1, 34 | orientation="vertical" 35 | ) 36 | 37 | def update(n: int): 38 | global x, y 39 | 40 | # Reset previous point 41 | points[y, x] = 0 42 | 43 | if n > 0: 44 | # Set next point 45 | x, y = XY[n] 46 | points[y, x] = 1 47 | 48 | # Redraw 49 | im.set_array(points) 50 | 51 | slider.on_changed(update) 52 | 53 | # Increment button 54 | ax_inc = plt.axes([0.8, 0.5, 0.15, 0.25]) # [left, bottom, width, height] 55 | button_inc = Button(ax_inc, '+1') 56 | 57 | def increment(event): 58 | if slider.val < slider.valmax: 59 | slider.set_val(slider.val + 1) 60 | else: 61 | slider.set_val(slider.valmin) 62 | 63 | button_inc.on_clicked(increment) 64 | 65 | # Decrement button 66 | ax_dec = plt.axes([0.8, 0.2, 0.15, 0.25]) # [left, bottom, width, height] 67 | button_dec = Button(ax_dec, '-1') 68 | 69 | def decrement(event): 70 | if slider.val > slider.valmin: 71 | slider.set_val(slider.val - 1) 72 | else: 73 | slider.set_val(slider.valmax) 74 | 75 | button_dec.on_clicked(decrement) 76 | 77 | # Show plot 78 | plt.show() 79 | -------------------------------------------------------------------------------- /local/ec/util.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, List, Iterable 2 | import unittest 3 | 4 | # Use this in conjunction with ec.core 5 | # from local.ec.core import Scalar, AffinePoint, ONE_POINT, NUMBER_POINTS 6 | # Point = AffinePoint 7 | 8 | # Use this in conjunction with ec.static 9 | from local.ec.static import Scalar, CurvePoint, ONE_POINT, NUMBER_POINTS 10 | Point = CurvePoint 11 | 12 | 13 | class Opening: 14 | """ 15 | Opening of a cryptographic commitment to a value. 16 | """ 17 | v: Scalar 18 | """ 19 | Contained value 20 | """ 21 | r: Scalar 22 | """ 23 | Blinding factor 24 | """ 25 | g: Point 26 | """ 27 | Generator for value 28 | """ 29 | h: Point 30 | """ 31 | Generator for blinding factor 32 | 33 | **Both generators must be independent from each other!** 34 | """ 35 | 36 | def __init__(self, v: Scalar, g: Point, h: Point): 37 | self.v = v 38 | self.g = g 39 | self.r = Scalar.random() 40 | self.h = h 41 | 42 | def __repr__(self) -> str: 43 | return "{}: {}".format(self.value(), self.close()) 44 | 45 | def value(self) -> Scalar: 46 | return self.v 47 | 48 | def close(self) -> Point: 49 | """ 50 | Return the commitment that corresponds to the opening. 51 | """ 52 | return self.h * self.r + self.g * self.v 53 | 54 | def verify(self, commitment: Point) -> bool: 55 | """ 56 | Return whether the given commitment corresponds to this opening. 57 | """ 58 | return commitment == self.close() 59 | 60 | @classmethod 61 | def batch_verify(cls, openings: "List[Opening]", commitments: List[Point]) -> bool: 62 | """ 63 | Verify that the list of openings opens to the list of commitments (in order). 64 | """ 65 | assert len(openings) == len(commitments) 66 | for i in range(len(openings)): 67 | if not openings[i].verify(commitments[i]): 68 | return False 69 | return True 70 | 71 | def serialize(self, compact: int = NUMBER_POINTS) -> Tuple[int, int]: 72 | """ 73 | Serialize the opening as it would be broadcast in an interactive proof. 74 | """ 75 | return int(self.v) % compact, int(self.r) % compact 76 | 77 | @classmethod 78 | def batch_serialize(cls, openings: "Iterable[Opening]", compact: int = NUMBER_POINTS) -> Tuple[Tuple[int, int], ...]: 79 | """ 80 | Serialize a list of openings as they would be broadcast in an interactive proof. 81 | """ 82 | return tuple([opening.serialize(compact) for opening in openings]) 83 | 84 | 85 | class TestOpening(unittest.TestCase): 86 | def test_hiding(self): 87 | one_point = ONE_POINT 88 | punto_uno, = Point.sample_greater_one(1) 89 | 90 | v = Scalar(2) 91 | c1 = Opening(v, one_point, punto_uno) 92 | c2 = Opening(v, one_point, punto_uno) 93 | 94 | self.assertNotEquals(c1.close(), c2.close()) 95 | -------------------------------------------------------------------------------- /cyclic_lagrange.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from matplotlib.widgets import Slider, Button 4 | 5 | # Initialize the plot 6 | fig, ax = plt.subplots() 7 | # Leave space for UI 8 | fig.subplots_adjust(bottom=0.25) 9 | 10 | # Parameters 11 | primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97] 12 | index = 0 13 | step = 1 14 | 15 | 16 | def update_plot(n_nodes: int, step: int): 17 | # Clear previous plot 18 | ax.cla() 19 | ax.axis('off') 20 | 21 | next_prime_text.set_text(f"Number of nodes: {primes[index]}") 22 | step_text.set_text(f"Step size: {step}") 23 | 24 | # Calculate node coordinates 25 | theta = np.linspace(0, 2*np.pi, n_nodes, endpoint=False) 26 | x = np.cos(theta) 27 | y = np.sin(theta) 28 | 29 | # Draw nodes 30 | for i in range(n_nodes): 31 | ax.plot(x[i], y[i], 'o', markersize=10) 32 | 33 | # Draw edges 34 | i = step % n_nodes 35 | 36 | for number in range(n_nodes): 37 | j = (i + step) % n_nodes 38 | ax.plot([x[i], x[j]], [y[i], y[j]], 'k-') 39 | ax.text(x[i] * 1.1, y[i] * 1.1, str((number + 1) % n_nodes), horizontalalignment='center') 40 | i = j 41 | 42 | # Draw plot 43 | plt.draw() 44 | 45 | 46 | def next_prime(_): 47 | global index, step 48 | if index < len(primes) - 1: 49 | index += 1 50 | step = 1 51 | update_plot(primes[index], step) 52 | 53 | 54 | def previous_prime(_): 55 | global index, step 56 | if index > 0: 57 | index -= 1 58 | step = 1 59 | update_plot(primes[index], step) 60 | 61 | 62 | def next_step(_): 63 | global step 64 | if step < primes[index] - 1: 65 | step += 1 66 | update_plot(primes[index], step) 67 | 68 | 69 | def previous_step(_): 70 | global step 71 | if step > 0: 72 | step -= 1 73 | update_plot(primes[index], step) 74 | 75 | 76 | # Buttons for changing parameters 77 | ax_next_prime = fig.add_axes([0.1, 0.1, 0.2, 0.03]) 78 | ax_previous_prime = fig.add_axes([0.3, 0.1, 0.2, 0.03]) 79 | ax_next_step = fig.add_axes([0.5, 0.1, 0.2, 0.03]) 80 | ax_previous_step = fig.add_axes([0.7, 0.1, 0.2, 0.03]) 81 | 82 | next_prime_button = Button(ax=ax_next_prime, label="Next prime") 83 | previous_prime_button = Button(ax=ax_previous_prime, label="Previous prime") 84 | next_step_button = Button(ax=ax_next_step, label="Next step") 85 | previous_step_button = Button(ax=ax_previous_step, label="Previous step") 86 | 87 | next_prime_button.on_clicked(next_prime) 88 | previous_prime_button.on_clicked(previous_prime) 89 | next_step_button.on_clicked(next_step) 90 | previous_step_button.on_clicked(previous_step) 91 | 92 | # Text showing current parameters 93 | next_prime_text = ax_next_prime.text(0.4, -0.1, "", ha="center", transform=ax.transAxes, verticalalignment="top") 94 | step_text = ax_next_prime.text(0.6, -0.1, "", ha="center", transform=ax.transAxes, verticalalignment="top") 95 | 96 | # Show plot 97 | update_plot(primes[index], 1) 98 | plt.show() 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | -------------------------------------------------------------------------------- /expectations.md: -------------------------------------------------------------------------------- 1 | # Setting expectations 2 | 3 | It is important to set the right expectations, going into the workshop, doing the workshop and going out of the workshop. 4 | 5 | ## How you go into the workshop 6 | 7 | While anybody can do the workshop, there are some minimal requirements that you should meet in order to benefit from the lessons. 8 | 9 | | Recommended ✅ | Minimal ⚠️ | Reconsider? ❌ | 10 | |-------------------------------------------------------------------|--------------------------------------------------------------------|----------------------------------| 11 | | I really want to learn about ZKP. | I want to learn something new. | Learning sounds like work. | 12 | | I want to do the entire workshop, which might take days to weeks. | I want to do a few chapters, which might take a few hours. | I am in a hurry. | 13 | | I want to experiment with the ZKP mathematics. | I want to hear about the mathematical concepts that make ZKP work. | I vowed to never do maths again. | 14 | | I want to write my own ZKP code. | I want to see the code that implements ZKP. | Seeing code makes me sick. | 15 | 16 | ## How we do the workshop 17 | 18 | We do the workshop in a particular style. There are certain standards of quality that we try to uphold. 19 | 20 | - **First principles:** No prior knowledge beyond a high-school education is required. 21 | - **Rediscovery:** Instead of stating raw facts, we retrace the steps that led to the invention of something. Why do we need this? Why does this simple version fail? How can we fix it? 22 | - **Informal proofs:** We will learn through interactive tools and empirical experiments. There will be as few formulas as possible. Intuition is more important than exact details. 23 | - **[Reasoning transparency](https://www.openphilanthropy.org/research/reasoning-transparency/):** There should be a complete argument for every claim in this workshop. You shouldn't have to trust me to come up with your own opinion. 24 | - **Everything is code:** Everything we will see is written in code. Most of the code lives in this repository. You can take a look at it, learn from it, modify it and play with it. 25 | 26 | We often fail to achieve the ideal, but this is our North Star. 27 | 28 | Feel free to reach out or to open a GitHub issue if there is room for improvement. 29 | 30 | ## How you go out of the workshop 31 | 32 | After going through the workshop, you should be able to do the following things. 33 | 34 | | Mastery 🏆 | Success 🎯 | 35 | |-------------------------------------------------------------------|-------------------------------------------------------------| 36 | | I understand a range of ZKP protocols. | I have an intuition why a range of ZKP protocols works. | 37 | | I can explain a range of ZKP protocols to a friend. | I can explain the concept of ZKP to a friend. | 38 | | I can construct a ZKP protocol for similar problems. | I can name the usual tricks that make ZKP protocols work. | 39 | | I can spot typical errors if someone shows me their ZKP protocol. | I can name typical errors that ZKP protocols have to avoid. | 40 | 41 | If you find yourself on the left-hand side of the table, then congratulations, you mastered the workshop! I couldn't ask for more. 42 | 43 | If you find yourself on the right-hand side, then also congrats, you successfully did the workshop! You could go deeper into the topic, but you don't have to. 44 | 45 | If you find yourself nowhere on the table, then something went wrong. Feel free to ask for help. If there is a problem with a lesson, then feel free to open a GitHub issue. 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting to Know Zero-Knowledge 2 | 3 | Everyone talks about zero-knowledge proofs (ZKP), but who understands them? How can a proof consist of bytes without leaking any knowledge? Why should I trust such a proof? 🤔 4 | 5 | ## What this is 6 | 7 | This is a workshop about ZKP written in Python. We use Jupyter as our interactive learning environment. Each notebook is a chapter. The chapters cover fundamentals, applications and much more. Explore and have fun 😜 8 | 9 | ## What this _isn't_ 10 | 11 | We will not cover the latest and shiniest crypto. We will not build SNARKs, STARKs or validity rollups. The code you will see will not live up to the highest security standards _(we use insecure parameters and take shortcuts for easier learning)_ 🕵️ 12 | 13 | ## Rationale 14 | 15 | Master the basics first, then move to the advanced stuff; that is my philosophy. Let's build an intuition for how ZKP works. We start small and work our way up. Things become simpler when we break them down into their constituent parts. Divide and conquer. Once there is understanding, we can take what we learned here and apply it to real problems 💪 16 | 17 | ## Benefits of zero-knowledge proofs 18 | 19 | Zero-knowledge proofs have clear benefits compared to ordinary plain-text proofs. These properties might seem impossible and have the potential to change our world. 20 | 21 | ### Compact 22 | 23 | Proofs grow logarithmically in size. If you double the size of the data, then the size of the proof grows by a single bit. This means, we can **compress data beyond entropy**! 24 | 25 | ### Fast 26 | 27 | Proofs take logarithmic time to verify. If you double the number of steps of a computation, then the verification of the proof only takes one step longer! This means, we can **compress computations**! 28 | 29 | ### Zero-knowledge 30 | 31 | Proofs reveal no information. Everything that is sent over the wire is randomized. This means, we can **work on private data without revealing it**! 32 | 33 | ## Run the workshop 34 | 35 | Run the workshop online (binder) or locally (nix or pip). 36 | 37 | We have come a long way since this workshop Sage (1 GB). The new dependencies take up less than 50 MB 🍃 38 | 39 | See below how to set up the workshop. 40 | 41 | Then select a chapter to read 📖 42 | 43 | ### Use binder 44 | 45 | Click the binder badge and wait for the workshop to be built on the server. 46 | 47 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/uncomputable/zkp-workshop/master) 48 | 49 | ### Use nix 50 | 51 | Use the provided nix flake to set up the runtime environment. 52 | 53 | ``` 54 | nix develop 55 | ``` 56 | 57 | Run Jupyter on the command line. 58 | 59 | ``` 60 | jupyter notebook 61 | ``` 62 | 63 | ### Use pip 64 | 65 | Create a virtual environment and use pip to install the dependencies. 66 | 67 | ``` 68 | python3 -m venv venv && source venv/bin/activate 69 | pip install -r requirements.txt 70 | ``` 71 | 72 | Run Jupyter on the command line. 73 | 74 | ``` 75 | jupyter notebook 76 | ``` 77 | 78 | ## Read the chapters 79 | 80 | Some chapters depend on lessons from other chapters. 81 | 82 | Start with chapters that depend on nothing else and work your way down the dependency tree, towards more advanced chapters. 83 | 84 | Skip / skim lessons you already know. Look at what interests you and ignore everything else. Have fun 🤓 85 | 86 | ![Chapter dependency tree](chapter_dependencies.svg) 87 | 88 | ## Explore extra content 89 | 90 | ### See why the discrete logarithm is hard 91 | 92 | See how the curve points jump in 2D space as you iterate through the curve. 93 | 94 | This is explained in more detail in the chapter on elliptic curves 🌀 95 | 96 | ``` 97 | python3 hardness_dlog.py 98 | ``` 99 | 100 | ### Play Sudoku 101 | 102 | Play the side of Victor in an interactive proof of knowledge of a Sudoku solution. Accept or reject. Peggy might be lying! 🧩 103 | 104 | ``` 105 | python3 play_sudoku.py 106 | ``` 107 | 108 | ### Customize the workshop 109 | 110 | Look at [the documentation](https://github.com/uncomputable/zkp-workshop/blob/master/customization.md) for how to further customize the workshop 🎨 111 | 112 | ## Improve the workshop 113 | 114 | If you see errors or room for improvement, then feel free to open a Github issue. 115 | 116 | Pull requests are also welcome. 117 | 118 | Let's turn this workshop into the best it can be 🚀 119 | 120 | ## Continue your journey 121 | 122 | There is a lot more to learn about ZKP. 123 | 124 | Check out these external resources. Happy learning 🧠 125 | 126 | - [Number theory explained from first principles](https://explained-from-first-principles.com/number-theory/) 127 | - [Tackling bulletproofs](https://github.com/uncomputable/tackling-bulletproofs) 128 | - [ZKP explained in 3 examples](https://www.circularise.com/blogs/zero-knowledge-proofs-explained-in-3-examples) 129 | - [Computer scientist explains ZKP in 5 levels of difficulty](https://www.youtube.com/watch?v=fOGdb1CTu5c) 130 | - [How to explain ZKP to your children](https://pages.cs.wisc.edu/~mkowalcz/628.pdf) 131 | -------------------------------------------------------------------------------- /local/graph.py: -------------------------------------------------------------------------------- 1 | import random 2 | import networkx as nx 3 | from typing import Dict, List, Tuple, TypeVar, Generic, Iterator 4 | 5 | 6 | def random_graph(n: int, e: int) -> nx.Graph: 7 | """ 8 | Return a random graph with [n] nodes and [e] edges. 9 | """ 10 | if e + 1 < n: 11 | raise ValueError("Need enough edges to connect all nodes") 12 | if e > n * (n - 1) / 2: 13 | raise ValueError("Too many edges for too few nodes") 14 | 15 | return nx.gnm_random_graph(n, e) 16 | 17 | 18 | def non_isomorphic_graph(first: nx.Graph) -> nx.Graph: 19 | """ 20 | Return a random graph that is not isomorphic to the [first] graph. 21 | 22 | The returned graph has the same number of nodes and edges. 23 | """ 24 | n = len(first.nodes) 25 | e = len(first.edges) 26 | if e == n * (n - 1) / 2: 27 | raise ValueError("Graph is complete: There are only isomorphic graphs with the same number of nodes and edges") 28 | 29 | while True: 30 | second = nx.gnm_random_graph(n, e) 31 | if not nx.is_isomorphic(first, second): 32 | return second 33 | 34 | 35 | def three_colorable_graph(n: int) -> Tuple[nx.Graph, Dict[int, int]]: 36 | """ 37 | Return a random graph with at least [n] nodes that is three-colorable. 38 | Also return the coloring 39 | """ 40 | graph = nx.cycle_graph(n) 41 | coloring = nx.coloring.greedy_color(graph, strategy="largest_first") 42 | coloring = {node: color % 3 for node, color in coloring.items()} 43 | 44 | return graph, coloring 45 | 46 | 47 | def not_three_colorable_graph(n: int) -> Tuple[nx.Graph, Dict[int, int]]: 48 | """ 49 | Return a random graph with at least [n] nodes that is not three-colorable. 50 | Also return a fake coloring. 51 | """ 52 | n = max(4, n) 53 | 54 | graph = nx.circulant_graph(n, offsets=[1, 2, 3]) 55 | coloring = nx.coloring.greedy_color(graph, strategy="largest_first") 56 | coloring = {node: color % 3 for node, color in coloring.items()} 57 | 58 | return graph, coloring 59 | 60 | 61 | A = TypeVar("A") 62 | B = TypeVar("B") 63 | C = TypeVar("C") 64 | 65 | 66 | class Mapping(Generic[A, B]): 67 | """ 68 | Mapping of values: 69 | 70 | Values from the domain A are mapped onto values from the codomain B. 71 | 72 | Each input from A is mapped to exactly one output from B, or A is undefined (function). 73 | 74 | Each output from B is the result of mapping some input A onto it (surjective). 75 | 76 | There might be distinct inputs that map onto the same output (not injective), 77 | or each input maps onto a unique output (injective). 78 | 79 | If the mapping is injective, then (because it is always surjective) it is a bijection, aka a one-to-one mapping. 80 | """ 81 | inner: Dict[A, B] 82 | 83 | def __init__(self, inner: Dict[A, B]): 84 | self.inner = inner 85 | 86 | def __repr__(self) -> str: 87 | return str(self.inner) 88 | 89 | def __iter__(self) -> A: 90 | for key in self.inner: 91 | yield key 92 | 93 | def __getitem__(self, index: A) -> B: 94 | return self.inner[index] 95 | 96 | def __len__(self) -> int: 97 | return len(self.inner) 98 | 99 | @classmethod 100 | def shuffle_graph(cls, graph: nx.Graph) -> "Mapping[int, int]": 101 | """ 102 | Create a mapping of node labels by random shuffling nodes. 103 | 104 | :param graph: original graph 105 | :return: random shuffling of node labels 106 | """ 107 | original = list(graph.nodes()) 108 | shuffled = original.copy() 109 | random.shuffle(shuffled) 110 | inner = {original[i]: shuffled[i] for i in range(len(original))} 111 | 112 | return Mapping(inner) 113 | 114 | @classmethod 115 | def shuffle_list(cls, lst: List[A]) -> "Mapping[A, A]": 116 | """ 117 | Create a mapping by randomly shuffling list elements. 118 | 119 | The original list remains unchanged. 120 | 121 | :param lst: original list 122 | :return: random shuffling of list elements 123 | """ 124 | shuffled = lst.copy() 125 | random.shuffle(shuffled) 126 | inner = {lst[i]: shuffled[i] for i in range(len(lst))} 127 | 128 | return Mapping(inner) 129 | 130 | def apply_graph(self, graph: nx.Graph) -> nx.Graph: 131 | """ 132 | Apply the mapping of node labels to a graph. 133 | 134 | The original graph remains unchanged and a new graph is crated. 135 | 136 | :param graph: original graph 137 | :return: graph with mapped node labels 138 | """ 139 | return nx.relabel_nodes(graph, self.inner, copy=True) 140 | 141 | def apply_list(self, lst: List[A]) -> List[A]: 142 | """ 143 | Apply the mapping to a list. 144 | 145 | The original list remains unchanged and a new list is created. 146 | 147 | :param lst: original list 148 | :return: list of mapped values 149 | """ 150 | return [self.inner[x] if x in self.inner else x for x in lst] 151 | 152 | def is_bijection(self) -> bool: 153 | """ 154 | Return whether the mapping is a bijection: 155 | 156 | Each input from A is mapped to a unique output from B. 157 | 158 | :return: mapping is a bijection 159 | """ 160 | return len(self.inner) == len(set(self.inner.values())) 161 | 162 | def invert(self) -> "Mapping[B, A]": 163 | """ 164 | Invert the mapping. 165 | 166 | If `a → b` in this mapping, then `b → a` in the inverse. 167 | 168 | **Only bijections are invertible!** 169 | 170 | :return: inverse mapping 171 | """ 172 | if not self.is_bijection(): 173 | raise ValueError("Can only invert bijective mappings") 174 | 175 | inner = {v: k for k, v in self.inner.items()} 176 | return Mapping(inner) 177 | 178 | def and_then(self, second: "Mapping[B, C]") -> "Mapping[A, C]": 179 | """ 180 | Compose this mapping with a second mapping. 181 | 182 | If `a → b` in this mapping and `b → a` in the second mapping, 183 | then `a → c` in the composition. 184 | 185 | If `a → b` in this mapping and `b` is undefined for the second mapping, 186 | then `a` is undefined in the composition. 187 | 188 | :param second: second mapping 189 | :return: composed mapping 190 | """ 191 | inner = {k: second.inner[self.inner[k]] for k in self.inner if self.inner[k] in second.inner} 192 | return Mapping(inner) 193 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /local/primes.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, List 2 | from functools import reduce 3 | from operator import mul 4 | from local.ec.core import MAX_COORDINATE, NUMBER_POINTS 5 | import random 6 | import math 7 | import unittest 8 | 9 | 10 | def miller_rabin(n: int, k: int) -> bool: 11 | """ 12 | Return whether n is probably prime after k rounds. 13 | 14 | Uses the Miller-Rabin primality test. 15 | https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test 16 | 17 | Probability that composite number is declared prime after k rounds: 18 | Pr(MR_k | ¬P) <= 4 ** (-k) 19 | 20 | Probability that uniform random number between 0 and n is prime: 21 | Pr(P_n) = log(n) / n 22 | https://en.wikipedia.org/wiki/Prime_number_theorem 23 | 24 | Probability that declared prime (<= n) is in fact composite after k rounds: 25 | Pr(¬P | MR_k) < Pr(MR_k | ¬P) * (1 / Pr(P_n) - 1) 26 | 27 | For n <= 1000: 28 | Pr(¬P | MR_1) < 0.002 29 | Pr(¬P | MR_2) < 0.0004 30 | Pr(¬P | MR_5) < 6.74 * 10**(-6) 31 | Pr(¬P | MR_10) < 6.58 * 10**(-9) 32 | """ 33 | if n <= 1 or n == 4: 34 | return False 35 | if n <= 3: 36 | return True 37 | 38 | # Factor out powers of 2 to find s > 0 and d > 0 39 | # such that n - 1 = d * 2^s with d odd 40 | d, s = n - 1, 0 41 | while d % 2 == 0: 42 | s += 1 43 | d //= 2 44 | assert n - 1 == d * 2 ** s 45 | 46 | for _ in range(0, k): 47 | a = random.randrange(2, n - 2) 48 | x = pow(a, d, n) 49 | y = 0 50 | 51 | for _ in range(0, s): 52 | y = pow(x, 2, n) 53 | if y == 1 and x != 1 and x != n - 1: 54 | return False 55 | x = y 56 | 57 | if y != 1: 58 | return False 59 | 60 | return True 61 | 62 | 63 | def trial_division(n: int) -> bool: 64 | """ 65 | Return whether n is provably prime. 66 | 67 | Uses a simple optimized trial division method. 68 | https://en.wikipedia.org/wiki/Primality_test 69 | """ 70 | if n <= 3: 71 | return n > 1 72 | if n % 2 == 0 or n % 3 == 0: 73 | return False 74 | limit = math.isqrt(n) 75 | for i in range(5, limit + 1, 6): 76 | if n % i == 0 or n % (i + 2) == 0: 77 | return False 78 | return True 79 | 80 | 81 | def is_prime(n: int) -> bool: 82 | """ 83 | Return whether n is prime. 84 | """ 85 | return miller_rabin(n, 5) 86 | 87 | 88 | def euler_totient(factors: Iterable[int]) -> int: 89 | """ 90 | Compute Euler's totient function of integer n whose prime factorization is given. 91 | 92 | **The method assumes that the given factors are prime numbers!** 93 | """ 94 | n = reduce(mul, factors, 1) 95 | return int(n * reduce(mul, [1 - 1 / p for p in set(factors)], 1)) 96 | 97 | 98 | def is_coprime(a: int, factors: Iterable[int]) -> bool: 99 | """ 100 | Check if integer a is a coprime of integer n whose prime factorization is given. 101 | 102 | **The method assumes that the given factors are prime numbers!** 103 | """ 104 | return all(a % p != 0 for p in factors) 105 | 106 | 107 | def get_coprimes(factors: Iterable[int]) -> List[int]: 108 | """ 109 | Return the list of coprimes of integer n whose prime factorization is given. 110 | 111 | This is equivalent to the elements of the multiplicative group of integers modulo n. 112 | 113 | **The method assumes that the given factors are prime numbers!** 114 | """ 115 | n = reduce(mul, factors, 1) 116 | return [a for a in range(1, n) if is_coprime(a, factors)] 117 | 118 | 119 | def get_coprime(factors: Iterable[int]) -> int: 120 | """ 121 | Return a random coprime of integer n whose prime factorization is given. 122 | 123 | **The method assumes that the given factors are prime numbers!** 124 | """ 125 | n = reduce(mul, factors, 1) 126 | while True: 127 | a = random.randrange(1, n) 128 | if is_coprime(a, factors): 129 | return a 130 | 131 | 132 | class TestPrimes(unittest.TestCase): 133 | primes = [ 134 | 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 135 | 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 136 | 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 137 | 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 138 | 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 139 | 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 140 | 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 141 | 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 142 | 983, 991, 997 143 | ] 144 | euler_totients = [ 145 | 1, 1, 2, 2, 4, 2, 6, 4, 6, 4, 10, 4, 12, 6, 8, 8, 16, 6, 18, 8, 12, 10, 22, 8, 20, 12, 18, 12, 28, 8, 30, 16, 146 | 20, 16, 24, 12, 36, 18, 24, 16, 40, 12, 42, 20, 24, 22, 46, 16, 42, 20, 32, 24, 52, 18, 40, 24, 36, 28, 58, 16, 147 | 60, 30, 36, 32, 48, 20, 66, 32, 44, 24, 70, 24, 72, 36, 40, 36, 60, 24, 78, 32, 54, 40, 82, 24, 64, 42, 56, 40, 148 | 88, 24, 72, 44, 60, 46, 72, 32, 96, 42, 60, 40 149 | ] 150 | 151 | def test_trial_division(self): 152 | for n in range(0, 1000): 153 | self.assertEqual(n in self.primes, trial_division(n)) 154 | 155 | def test_miller_rabin(self): 156 | for n in range(0, 1000): 157 | provably_prime = n in self.primes 158 | probably_prime = miller_rabin(n, 2) 159 | 160 | if provably_prime: 161 | self.assertTrue(probably_prime) 162 | if not probably_prime: 163 | self.assertFalse(provably_prime) 164 | 165 | if provably_prime and (not probably_prime): 166 | print("false negative: {}".format(n)) 167 | if probably_prime and (not provably_prime): 168 | print("false positive: {}".format(n)) 169 | 170 | def test_max_coordinate_is_prime(self): 171 | self.assertTrue(is_prime(MAX_COORDINATE)) 172 | 173 | def test_number_points_is_prime(self): 174 | self.assertTrue(is_prime(NUMBER_POINTS)) 175 | 176 | def test_euler_totient(self): 177 | primes = [p for p in self.primes if p < 100] 178 | 179 | for a in primes: 180 | self.assertEqual(self.euler_totients[a - 1], euler_totient([a])) 181 | for b in primes: 182 | if a * b > 100: 183 | break 184 | self.assertEqual(self.euler_totients[a * b - 1], euler_totient([a, b])) 185 | for c in primes: 186 | if a * b * c > 100: 187 | break 188 | self.assertEqual(self.euler_totients[a * b * c - 1], euler_totient([a, b, c])) 189 | 190 | def test_get_coprimes(self): 191 | primes = [p for p in self.primes if p < 100] 192 | 193 | for a in primes: 194 | for b in primes: 195 | n = a * b 196 | z_n_star = [i for i in range(n) if math.gcd(i, n) == 1] 197 | self.assertEqual(z_n_star, get_coprimes([a, b])) 198 | -------------------------------------------------------------------------------- /local/exact_cover.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from typing import Set, List, Dict, TypeVar, Iterator, Generic 3 | import unittest 4 | import bisect 5 | 6 | Row = TypeVar("Row") 7 | """ 8 | Row label. 9 | """ 10 | Col = TypeVar("Col") 11 | """ 12 | Column label. 13 | """ 14 | 15 | 16 | class Matrix(Generic[Row, Col]): 17 | """ 18 | Instance of the exact cover problem: 19 | 20 | Given a set X and a collection S of subsets of X, 21 | find a subcollection S* of S such that each element from X occurs exactly once in (a subset in) S*. 22 | We call S* a partition of X. 23 | 24 | This can be represented as a matrix. 25 | Each subset is a column. Each element is a row. 26 | The cell (element, subset) is 1 if element is contained in subset, and 0 otherwise. 27 | A solution is a collection of rows where each column is 1 in exactly one row. 28 | 29 | https://en.wikipedia.org/wiki/Exact_cover 30 | """ 31 | rows: Dict[Row, List[Col]] 32 | """ 33 | Maps each row label to a list of labels of columns where this row is member. 34 | 35 | This holds redundant data that we use to (un)cover columns. 36 | """ 37 | cols: Dict[Col, Set[Row]] 38 | """ 39 | Maps each column label to the set of labels of rows that are member. 40 | 41 | This is what we use most of the time. 42 | """ 43 | 44 | def __init__(self): 45 | self.rows = defaultdict(list) 46 | self.cols = defaultdict(set) 47 | 48 | def __repr__(self) -> str: 49 | return repr(self.rows) 50 | 51 | def __eq__(self, other: "Matrix") -> bool: 52 | return self.rows == other.rows and self.cols == other.cols 53 | 54 | def is_empty(self) -> bool: 55 | """ 56 | Return whether the matrix is empty. 57 | """ 58 | return len(self.cols) == 0 59 | 60 | def add_row(self, row: Row, cols: List[Col]): 61 | """ 62 | Add a row to the matrix. 63 | 64 | :param row: row label 65 | :param cols: list of labels of columns where this row is member 66 | """ 67 | self.rows[row] = cols 68 | for col in cols: 69 | self.cols[col].add(row) 70 | 71 | def add_column(self, col: Col, rows: Set[Row]): 72 | """ 73 | Add a column to the matrix. 74 | 75 | :param col: column label 76 | :param rows: set of labels of rows that are member 77 | """ 78 | self.cols[col] = rows 79 | for row in rows: 80 | bisect.insort(self.rows[row], col) 81 | 82 | def choose_column(self) -> Col: 83 | """ 84 | Choose the next column to work on. 85 | 86 | This is a deterministic choice, so we return exactly one column. 87 | 88 | :return: next column 89 | """ 90 | return min(self.cols, key=lambda i: len(self.cols[i])) 91 | 92 | def choose_row(self, c: Col) -> Iterator[Row]: 93 | """ 94 | Choose the next row to work on. 95 | 96 | Because this is a nondeterministic choice, we have to iterate over all possible rows. 97 | 98 | :param c: chosen column 99 | :return: iterator of next rows 100 | """ 101 | for r in list(self.cols[c]): 102 | yield r 103 | 104 | def cover_column(self, r: Row) -> List[Set[Row]]: 105 | """ 106 | Remove row r from the matrix. 107 | 108 | :param r: chosen row 109 | :return: list of removed columns 110 | """ 111 | removed_cols = [] 112 | 113 | # Delete all columns j inside row r (where r[j] == 1) 114 | # Because row r already covers them 115 | for j in self.rows[r]: 116 | # Delete all rows i inside column j (where i[j] == 1) 117 | # Because row i would cover the same column as row r 118 | # We keep self.rows constant and remove i from self.cols instead 119 | for i in self.cols[j]: 120 | for col in self.rows[i]: 121 | if col != j: # Cannot change self.cols[j] while iterating over it 122 | self.cols[col].remove(i) 123 | 124 | removed_cols.append(self.cols.pop(j)) 125 | 126 | return removed_cols 127 | 128 | def uncover_column(self, removed_cols: List[Set[Row]]): 129 | """ 130 | Undo removing a particular row from the matrix. 131 | 132 | Because all the required information is contained in the removed columns, 133 | the removed row is not passed to this function. 134 | 135 | :param removed_cols: list of removed columns 136 | """ 137 | # Restore each removed column j 138 | for j in removed_cols: 139 | # Restore each row i inside column j (where i[j] == 1) 140 | for i in j: 141 | # Add row i to each column, including j 142 | for col in self.rows[i]: 143 | self.cols[col].add(i) 144 | 145 | def algorithm_x(self, selected_rows: List[Row] = None) -> Iterator[List[Row]]: 146 | """ 147 | Solve the exact cover problem. 148 | 149 | Uses Donald Knuth's Algorithm X. 150 | 151 | https://arxiv.org/pdf/cs/0011047.pdf 152 | 153 | The idea of using dictionaries to efficiently represent rows and columns is by Ali Assaf. 154 | This is much easier than implementing Dancing Links. 155 | 156 | https://www.cs.mcgill.ca/~aassaf9/python/algorithm_x.html 157 | 158 | :param selected_rows: 159 | :return: iterator over solutions 160 | """ 161 | if self.is_empty(): 162 | yield selected_rows 163 | 164 | c = self.choose_column() 165 | for r in self.choose_row(c): 166 | selected_rows.append(r) 167 | removed_cols = self.cover_column(r) 168 | 169 | for solution in self.algorithm_x(selected_rows): 170 | yield solution 171 | 172 | self.uncover_column(removed_cols) 173 | selected_rows.pop() 174 | 175 | 176 | class TestMatrix(unittest.TestCase): 177 | def get_matrix(self): 178 | matrix = Matrix() 179 | matrix.add_row("A", [1, 4, 7]) 180 | matrix.add_row("B", [1, 4]) 181 | matrix.add_row("C", [4, 5, 7]) 182 | matrix.add_row("D", [3, 5, 6]) 183 | matrix.add_row("E", [2, 3, 6, 7]) 184 | matrix.add_row("F", [2, 7]) 185 | return matrix 186 | 187 | def test_cover_uncover(self): 188 | matrix = self.get_matrix() 189 | 190 | for row in matrix.rows: 191 | cols = matrix.cover_column(row) 192 | matrix.uncover_column(cols) 193 | self.assertEqual(self.get_matrix(), matrix) 194 | 195 | def test_cover_until_empty(self): 196 | matrix = self.get_matrix() 197 | for row in matrix.rows: 198 | matrix.cover_column(row) 199 | self.assertTrue(matrix.is_empty()) 200 | 201 | def test_cover_b(self): 202 | expected = defaultdict(set) 203 | expected[2] = {"E", "F"} 204 | expected[3] = {"D", "E"} 205 | expected[5] = {"D"} 206 | expected[6] = {"D", "E"} 207 | expected[7] = {"E", "F"} 208 | 209 | matrix = self.get_matrix() 210 | matrix.cover_column("B") 211 | self.assertEqual(expected, matrix.cols) 212 | 213 | def test_algorithm_x(self): 214 | matrix = self.get_matrix() 215 | solution = next(matrix.algorithm_x([])) 216 | self.assertEqual(["B", "D", "F"], solution) 217 | -------------------------------------------------------------------------------- /local/ec/static.py: -------------------------------------------------------------------------------- 1 | import random 2 | import unittest 3 | from typing import Tuple, Union, List, Iterable 4 | 5 | MAX_COORDINATE = 7 6 | MINUS_ONE_COORDINATE = MAX_COORDINATE - 1 7 | NUMBER_POINTS = 13 8 | 9 | 10 | class CurvePoint: 11 | """ 12 | A point on the curve. 13 | 14 | In contrast to AffinePoint, this point is guaranteed to be on the curve. 15 | Arbitrary points in affine space are not supported. 16 | """ 17 | n: int 18 | """ 19 | Number of the point (aka its discrete logarithm). 20 | """ 21 | 22 | def __init__(self, n: int): 23 | self.n = n 24 | 25 | def xy(self) -> Tuple[int, int]: 26 | """ 27 | Return the xy coordinates of the point in affine space. 28 | """ 29 | return XY[self.n] 30 | 31 | def __repr__(self) -> str: 32 | if self.is_zero(): 33 | return "(zero)" 34 | else: 35 | return repr(self.xy()) 36 | 37 | def __hash__(self) -> hash: 38 | return hash(self.n) 39 | 40 | def __eq__(self, other: "CurvePoint") -> bool: 41 | return self.n == other.n 42 | 43 | def is_zero(self) -> bool: 44 | """ 45 | Return whether the point is zero. 46 | """ 47 | return self.n == 0 48 | 49 | def __add__(self, other: "CurvePoint") -> "CurvePoint": 50 | return CurvePoint((self.n + other.n) % NUMBER_POINTS) 51 | 52 | def __neg__(self) -> "CurvePoint": 53 | return CurvePoint(-self.n % NUMBER_POINTS) 54 | 55 | def __sub__(self, other: "CurvePoint") -> "CurvePoint": 56 | return CurvePoint((self.n - other.n) % NUMBER_POINTS) 57 | 58 | def __mul__(self, other: "Scalar") -> "CurvePoint": 59 | return CurvePoint((self.n * other.n) % NUMBER_POINTS) 60 | 61 | def discrete_log(self) -> "Scalar": 62 | """ 63 | Return the discrete logarithm of the point. 64 | 65 | This is a scalar n such that One * n = self. 66 | """ 67 | return Scalar(self.n) 68 | 69 | @classmethod 70 | def nth(cls, n: int) -> "CurvePoint": 71 | """ 72 | Return the n-th point on the curve. 73 | 74 | The integer n is internally scaled to the size of the curve. 75 | """ 76 | return CurvePoint(n % NUMBER_POINTS) 77 | 78 | @classmethod 79 | def random(cls) -> "CurvePoint": 80 | """ 81 | Return a uniformly random point on the curve. 82 | """ 83 | return CurvePoint(random.randrange(NUMBER_POINTS)) 84 | 85 | @classmethod 86 | def sample_greater_one(cls, n_sample: int) -> "List[CurvePoint]": 87 | """ 88 | Randomly sample distinct points on the curve that are greater than one (not zero and not one). 89 | """ 90 | return [CurvePoint(i) for i in random.sample(range(2, NUMBER_POINTS), n_sample)] 91 | 92 | def serialize(self, compact: int = NUMBER_POINTS) -> int: 93 | """ 94 | Serialize the point as an integer. 95 | """ 96 | if self.is_zero(): 97 | return (MAX_COORDINATE ** 2) % compact 98 | else: 99 | x, y = self.xy() 100 | return (x * MAX_COORDINATE + y) % compact 101 | 102 | @classmethod 103 | def batch_serialize(cls, points: "Iterable[CurvePoint]", compact: int = NUMBER_POINTS) -> "Tuple[int, ...]": 104 | """ 105 | Serialize a list of points as integers. 106 | """ 107 | return tuple([point.serialize(compact) for point in points]) 108 | 109 | 110 | ZERO_POINT = CurvePoint(0) 111 | """ 112 | Zero-point. That is the zeroth point on the curve or the point with a discrete logarithm of zero. 113 | 114 | The zero-point is always the same. 115 | """ 116 | ONE_POINT = CurvePoint(1) 117 | """ 118 | One-point. That is the first point on the curve or the point with a discrete logarithm of one. 119 | 120 | Note that the choice of the one-point is arbitrary. 121 | On our particular curve, every non-zero point could be the one-point. 122 | The other points are numbered according to how many times the one-point was added onto itself to arrive at that point. 123 | 124 | The static EC module has a hardcoded one-point. 125 | """ 126 | 127 | 128 | class Scalar: 129 | """ 130 | Scalar of the curve. 131 | 132 | That is an integer modulo the number of points. 133 | """ 134 | n: int 135 | """ 136 | Value of the scalar. 137 | """ 138 | 139 | def __init__(self, n: int): 140 | self.n = n 141 | 142 | def __int__(self) -> int: 143 | return self.n 144 | 145 | def __repr__(self) -> str: 146 | return repr(self.n) 147 | 148 | def __hash__(self) -> hash: 149 | return hash(self.n) 150 | 151 | def __eq__(self, other: "CurvePoint") -> bool: 152 | return self.n == other.n 153 | 154 | def __add__(self, other: "Scalar") -> "Scalar": 155 | return Scalar((self.n + other.n) % NUMBER_POINTS) 156 | 157 | def __neg__(self) -> "Scalar": 158 | return Scalar(-self.n % NUMBER_POINTS) 159 | 160 | def __sub__(self, other: "Scalar") -> "Scalar": 161 | return Scalar((self.n - other.n) % NUMBER_POINTS) 162 | 163 | def __mul__(self, other: "Scalar") -> "Scalar": 164 | return Scalar((self.n * other.n) % NUMBER_POINTS) 165 | 166 | def reciprocal(self) -> "Scalar": 167 | """ 168 | Return the multiplicative inverse of the scalar. 169 | 170 | This is a scalar i such that self * i = 1. 171 | """ 172 | return Scalar(pow(self.n, -1, NUMBER_POINTS)) 173 | 174 | def __truediv__(self, other: "Scalar") -> "Scalar": 175 | return self * other.reciprocal() 176 | 177 | def __pow__(self, power: Union[int, "Scalar"]) -> "Scalar": 178 | if isinstance(power, Scalar): 179 | power = power.n 180 | return Scalar(pow(self.n, power, NUMBER_POINTS)) 181 | 182 | @classmethod 183 | def nth(cls, n: int) -> "Scalar": 184 | """ 185 | Return the n-th scalar. 186 | 187 | The integer n is internally scaled to the size of the curve. 188 | """ 189 | return Scalar(n % NUMBER_POINTS) 190 | 191 | @classmethod 192 | def random(cls) -> "Scalar": 193 | """ 194 | Return a uniformly random scalar. 195 | """ 196 | return Scalar(random.randrange(NUMBER_POINTS)) 197 | 198 | def serialize(self, compact: int = NUMBER_POINTS) -> int: 199 | """ 200 | Serialize the scalar as an integer. 201 | """ 202 | return self.n % compact 203 | 204 | @classmethod 205 | def batch_serialize(cls, scalars: "Iterable[Scalar]", compact: int = NUMBER_POINTS) -> "Tuple[int, ...]": 206 | """ 207 | Serialize a list of scalars as an integer. 208 | """ 209 | return tuple([scalar.serialize(compact) for scalar in scalars]) 210 | 211 | 212 | XY = (None, (4, 2), (3, 3), (1, 2), (2, 5), (5, 3), (6, 3), (6, 4), (5, 4), (2, 2), (1, 5), (3, 4), (4, 5)) 213 | """ 214 | List of xy coordinates of all points in order (zeroth, first, second, ...). 215 | """ 216 | 217 | 218 | class TestCurvePoint(unittest.TestCase): 219 | def test_illegal_mul(self): 220 | # Scalar(a) * CurvePoint(b) = Scalar(a * b) 221 | # This multiplication is illegal because it requires knowledge of the discrete logarithm 222 | # We could solve the discrete logarithm problem if we had this multiplication 223 | # There are no type errors because we omit isinstance checks, which would be slow 224 | x = Scalar(2) * CurvePoint(3) 225 | self.assertEqual(Scalar(6), x) 226 | 227 | # CurvePoint(b) * Scalar(a) = CurvePoint(a * b) 228 | # This is proper scalar multiplication 229 | # The inner value is the same as above but the containing class is different 230 | y = CurvePoint(3) * Scalar(2) 231 | self.assertEqual(CurvePoint(6), y) 232 | -------------------------------------------------------------------------------- /chapter_dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | a->b 16 | 17 | 18 | Chapter dependency 19 | 20 | 21 | 22 | 23 | 24 | c->d 25 | 26 | 27 | Recommended path 28 | 29 | 30 | 31 | Basis/Interactive Proofs 32 | 33 | Basis/Interactive Proofs 34 | 35 | 36 | 37 | Easy/Schnorr 38 | 39 | Easy/Schnorr 40 | 41 | 42 | 43 | Basis/Interactive Proofs->Easy/Schnorr 44 | 45 | 46 | 47 | 48 | 49 | Basis/Commitments 50 | 51 | Basis/Commitments 52 | 53 | 54 | 55 | Basis/Interactive Proofs->Basis/Commitments 56 | 57 | 58 | 59 | 60 | 61 | Graph/Nonisomorphism 62 | 63 | Graph/Nonisomorphism 64 | 65 | 66 | 67 | Basis/Interactive Proofs->Graph/Nonisomorphism 68 | 69 | 70 | 71 | 72 | 73 | Graph/Isomorphism 74 | 75 | Graph/Isomorphism 76 | 77 | 78 | 79 | Basis/Interactive Proofs->Graph/Isomorphism 80 | 81 | 82 | 83 | 84 | 85 | Basis/Elliptic Curves 86 | 87 | Basis/Elliptic Curves 88 | 89 | 90 | 91 | Basis/Interactive Proofs->Basis/Elliptic Curves 92 | 93 | 94 | 95 | 96 | 97 | Easy/Schnorr->Graph/Nonisomorphism 98 | 99 | 100 | 101 | 102 | 103 | Graph/Colorability 104 | 105 | Graph/Colorability 106 | 107 | 108 | 109 | Basis/Commitments->Graph/Colorability 110 | 111 | 112 | 113 | 114 | 115 | Game/Sudoku 116 | 117 | Game/Sudoku 118 | 119 | 120 | 121 | Basis/Commitments->Game/Sudoku 122 | 123 | 124 | 125 | 126 | 127 | Basis/Commitments->Game/Sudoku 128 | 129 | 130 | 131 | 132 | 133 | Graph/Nonisomorphism->Basis/Commitments 134 | 135 | 136 | 137 | 138 | 139 | Basis/Elliptic Curves->Easy/Schnorr 140 | 141 | 142 | 143 | 144 | 145 | Basis/Elliptic Curves->Easy/Schnorr 146 | 147 | 148 | 149 | 150 | 151 | Basis/Elliptic Curves->Basis/Commitments 152 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /local/sudoku.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import unittest 4 | import logging 5 | from typing import List, Tuple, Iterator 6 | from local.exact_cover import Matrix 7 | 8 | 9 | class Board: 10 | """ 11 | Sudoku board. 12 | """ 13 | rows: List[List[int]] 14 | """ 15 | Rows of the board. 16 | 17 | Each row is a list of values for each column. 18 | """ 19 | dim: int 20 | """ 21 | Number of cells alongside a box. 22 | """ 23 | dim_sq: int 24 | """ 25 | Number of cells alongside the board. 26 | 27 | Equal to self.dim ** 2. 28 | """ 29 | 30 | def __init__(self, rows: List[List[int]]): 31 | self.rows = rows 32 | self.dim_sq = len(self.rows) 33 | self.dim = math.isqrt(self.dim_sq) 34 | 35 | def __repr__(self) -> str: 36 | return "\n".join([repr(row) for row in self.rows]) 37 | 38 | def __getitem__(self, row: int): 39 | return self.rows[row] 40 | 41 | @classmethod 42 | def blank(cls, dim: int) -> "Board": 43 | """ 44 | Return an empty board. 45 | 46 | :param dim: number of cells alongside a box; the board is dim**2 cells wide 47 | :return: blank board 48 | """ 49 | dim_sq = dim ** 2 50 | return Board([[0 for _ in range(dim_sq)] for _ in range(dim_sq)]) 51 | 52 | @classmethod 53 | def random(cls, dim: int) -> "Board": 54 | """ 55 | Return an empty board with some random presets filled in. 56 | 57 | :param dim: number of cells alongside a box; the board is dim**2 cells wide 58 | :return: random board 59 | """ 60 | dim_sq = dim ** 2 61 | rows = [[0 for _ in range(dim_sq)] for _ in range(dim_sq)] 62 | 63 | for value in range(1, dim + 1): 64 | row = random.randrange(dim_sq) 65 | col = random.randrange(dim_sq) 66 | rows[row][col] = value 67 | 68 | return Board(rows) 69 | 70 | def verify(self) -> bool: 71 | """ 72 | Verify that the board is a valid Sudoku solution. 73 | 74 | :return: board is valid solution 75 | """ 76 | for columns in self.rows: 77 | if not self.verify_area(columns): 78 | return False 79 | 80 | for col in range(self.dim_sq): 81 | rows = [self.rows[row][col] for row in range(self.dim_sq)] 82 | if not self.verify_area(rows): 83 | return False 84 | 85 | for box_row in range(0, self.dim_sq, self.dim): 86 | for box_col in range(0, self.dim_sq, self.dim): 87 | box = [self.rows[box_row + row_offset][box_col + col_offset] 88 | for row_offset in range(self.dim) for col_offset in range(self.dim)] 89 | if not self.verify_area(box): 90 | return False 91 | 92 | return True 93 | 94 | def verify_area(self, area: List[int]) -> bool: 95 | """ 96 | Verify that the area consists of unique, nonzero values. 97 | 98 | :param area: row, column or box 99 | :return: area is valid 100 | """ 101 | return 0 not in area and len(set(area)) == self.dim_sq 102 | 103 | def verify_shuffling(self, shuffled_values: Iterator[int]) -> bool: 104 | """ 105 | Verify that this board is consistent with the other board. 106 | 107 | Consistency means that there is a one-to-one mapping from the values of this board to values of the other board. 108 | If there is a mapping, then this board was obtained from the other one by shuffling values. 109 | If there is an inconsistency, then it is impossible to obtain this board from the other one. 110 | 111 | :param shuffled_values: iterator over nonzero values of other board 112 | :return: this board is consistent with other board 113 | """ 114 | mapping = {} 115 | 116 | for row, columns in enumerate(self.rows): 117 | for col, original_value in enumerate(columns): 118 | if original_value > 0: 119 | shuffled_value = next(shuffled_values) 120 | 121 | if original_value in mapping: 122 | # Mapped same value to different values 123 | if mapping[original_value] != shuffled_value: 124 | logging.info("Mapped same value to different values") 125 | return False 126 | else: 127 | mapping[original_value] = shuffled_value 128 | 129 | # Mapped different values to the same value 130 | if len(mapping.values()) != len(set(mapping.values())): 131 | logging.info("Mapped different values to same value") 132 | return False 133 | 134 | return True 135 | 136 | def to_matrix(self) -> Matrix[Tuple[int, int, int], str]: 137 | """ 138 | Convert the board into an instance of the exact cover problem. 139 | 140 | Possible value assignments for each cell are elements / rows. 141 | Constraints on the value assignments are subsets / columns. 142 | 143 | https://en.wikipedia.org/w/index.php?title=Exact_cover&oldid=1134545756#Sudoku 144 | 145 | :return: exact cover instance 146 | """ 147 | matrix = Matrix() 148 | 149 | # Each cell contains exactly one value 150 | for row in range(self.dim_sq): 151 | for col in range(self.dim_sq): 152 | matrix.add_column("r{}c{}".format(row, col), {(row, col, value) for value in range(1, self.dim_sq + 1)}) 153 | 154 | # Each row contains all values 155 | for row in range(self.dim_sq): 156 | for value in range(1, self.dim_sq + 1): 157 | matrix.add_column("r{}#{}".format(row, value), {(row, col, value) for col in range(self.dim_sq)}) 158 | 159 | # Each column contains all values 160 | for col in range(self.dim_sq): 161 | for value in range(1, self.dim_sq + 1): 162 | matrix.add_column("c{}#{}".format(col, value), {(row, col, value) for row in range(self.dim_sq)}) 163 | 164 | # Each box contains all values 165 | for box_row in range(0, self.dim_sq, self.dim): 166 | for box_col in range(0, self.dim_sq, self.dim): 167 | for value in range(1, self.dim_sq + 1): 168 | matrix.add_column( 169 | "b{}:{}#{}".format(box_row, box_col, value), 170 | {(box_row + row_offset, box_col + col_offset, value) for row_offset in range(self.dim) for col_offset in range(self.dim)} 171 | ) 172 | 173 | # Reduce matrix using preset values 174 | for row, columns in enumerate(self.rows): 175 | for col, value in enumerate(columns): 176 | if value > 0: 177 | matrix.cover_column((row, col, value)) 178 | 179 | return matrix 180 | 181 | def solve(self) -> "Board": 182 | """ 183 | Fill in the empty cells on the board to form a Sudoku solution. 184 | 185 | :return: Sudoku solution 186 | """ 187 | matrix = self.to_matrix() 188 | logging.info(f"Solving {len(matrix.rows)} elements and {len(matrix.cols)} constraints") 189 | assignment = next(matrix.algorithm_x([])) 190 | rows = [row.copy() for row in self.rows] 191 | 192 | for assigned_row in assignment: 193 | row, col, value = assigned_row 194 | rows[row][col] = value 195 | 196 | return Board(rows) 197 | 198 | def remove_values(self, n_remove: int) -> "Board": 199 | """ 200 | Remove cells from the board. 201 | 202 | :param n_remove: number of cells to be removed 203 | :return: copy of original board with cells removed 204 | """ 205 | rows = [row.copy() for row in self.rows] 206 | 207 | cells = random.sample([(row, col) for row in range(self.dim_sq) for col in range(self.dim_sq)], n_remove) 208 | for cell in cells: 209 | rows[cell[0]][cell[1]] = 0 210 | 211 | return Board(rows) 212 | 213 | def to_puzzle(self) -> "Board": 214 | """ 215 | Convert a complete Sudoku solution into a partial solution by removing values. 216 | 217 | :return: partial solution (copy) 218 | """ 219 | n_clues = math.ceil(self.dim_sq ** 2 * 0.2) 220 | logging.info(f"Sudoku with {n_clues} clues") 221 | return self.remove_values(self.dim_sq ** 2 - n_clues) 222 | 223 | def falsify_row(self): 224 | """ 225 | Create a duplicate value in a random row. 226 | """ 227 | row = random.randrange(self.dim_sq) 228 | logging.debug(f"Falsified row {row}") 229 | col0, col1 = random.sample(range(self.dim_sq), 2) 230 | assert col0 != col1 231 | self.rows[row][col0] = self.rows[row][col1] 232 | 233 | def falsify_column(self): 234 | """ 235 | Create a duplicate value in a random column. 236 | """ 237 | col = random.randrange(self.dim_sq) 238 | logging.debug(f"Falsified column {col}") 239 | row0, row1 = random.sample(range(self.dim_sq), 2) 240 | assert row0 != row1 241 | self.rows[row0][col] = self.rows[row1][col] 242 | 243 | def falsify_box(self): 244 | """ 245 | Create a duplicate value in a random box. 246 | """ 247 | box_row = random.randrange(self.dim) * self.dim 248 | box_col = random.randrange(self.dim) * self.dim 249 | logging.debug(f"Falsified box {box_row}:{box_col}") 250 | box = [(box_row + row_offset, box_col + col_offset) for row_offset in range(self.dim) for col_offset in range(self.dim)] 251 | 252 | cell0, cell1 = random.sample(box, 2) 253 | assert cell0 != cell1 254 | self.rows[cell0[0]][cell0[1]] = self.rows[cell1[0]][cell1[1]] 255 | 256 | def falsify(self, n_errors: int): 257 | """ 258 | Randomly create duplicate values to falsify a Sudoku solution. 259 | 260 | :param n_errors: number of errors 261 | """ 262 | for _ in range(n_errors): 263 | selection = random.randrange(3) 264 | if selection == 0: 265 | self.falsify_row() 266 | elif selection == 1: 267 | self.falsify_column() 268 | else: 269 | self.falsify_box() 270 | 271 | 272 | class TestBoard(unittest.TestCase): 273 | def test_solve_random(self): 274 | board = Board.random(3) 275 | solved = board.solve() 276 | assert solved.verify() 277 | 278 | def test_solve_hard(self): 279 | hard = Board([ 280 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 281 | [0, 0, 0, 0, 0, 3, 0, 8, 5], 282 | [0, 0, 1, 0, 2, 0, 0, 0, 0], 283 | [0, 0, 0, 5, 0, 7, 0, 0, 0], 284 | [0, 0, 4, 0, 0, 0, 1, 0, 0], 285 | [0, 9, 0, 0, 0, 0, 0, 0, 0], 286 | [5, 0, 0, 0, 0, 0, 0, 7, 3], 287 | [0, 0, 2, 0, 1, 0, 0, 0, 0], 288 | [0, 0, 0, 0, 4, 0, 0, 0, 9] 289 | ]) 290 | """ 291 | A Sudoku designed to work against the brute force algorithm. 292 | 293 | https://www.flickr.com/photos/npcomplete/2361922699 294 | """ 295 | solved = hard.solve() 296 | assert solved.verify() 297 | 298 | def test_falsify_verify(self): 299 | board = Board.random(3) 300 | solved = board.solve() 301 | solved.falsify(1) 302 | assert not solved.verify() 303 | 304 | def test_repeated_falsify(self): 305 | board = Board.random(3) 306 | solved = board.solve() 307 | solved.falsify(40) 308 | -------------------------------------------------------------------------------- /basis/commitments.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "b2a4a05c", 6 | "metadata": {}, 7 | "source": [ 8 | "# Commitments\n", 9 | "\n", 10 | "In this chapter we look at cryptographic commitments.\n", 11 | "\n", 12 | "This primitive is used basically all interactive proofs because it is so useful." 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "69f553c6", 18 | "metadata": {}, 19 | "source": [ 20 | "# TL;DR\n", 21 | "\n", 22 | "A commitment is an encrypted value. The creator of the commitment can decrypt (\"open\") the commitment.\n", 23 | "\n", 24 | "The commitment does not reveal anything about its contained value (hiding property).\n", 25 | "\n", 26 | "The commitment can only be opened to the value that was originally encrypted (binding property).\n", 27 | "\n", 28 | "Usually Peggy makes a large number of commitments and sends them to Victor. These commitments cover all possible cases of the statement to be proved. Victor randomly chooses a case. Peggy responds by opening a small number of corresponding commitments.\n", 29 | "\n", 30 | "Because of the setup, Peggy has to be able to respond to all possible cases. If Peggy cannot respond to one case, then Victor might choose that one. Because of the commitments, Peggy cannot undo what she sent and Victor will expose her lying.\n", 31 | "\n", 32 | "This saves bandwidth because only one case out of all possible cases is sent over the wire." 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "id": "ad4ba007", 38 | "metadata": {}, 39 | "source": [ 40 | "# Jupyter setup\n", 41 | "\n", 42 | "Run the following snippet to set up your jupyter notebook for the workshop." 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "id": "f95a0ea6", 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "import sys\n", 53 | "\n", 54 | "# Add project root so we can import local modules\n", 55 | "root_dir = sys.path.append(\"..\")\n", 56 | "sys.path.append(root_dir)\n", 57 | "\n", 58 | "# Import here so cells don't depend on each other\n", 59 | "from typing import List, Tuple\n", 60 | "from local.ec.util import Opening\n", 61 | "from local.ec.static import Scalar, CurvePoint, ONE_POINT\n", 62 | "import random" 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "id": "639daa5d", 68 | "metadata": {}, 69 | "source": [ 70 | "# Pedersen commitments (attempt)\n", 71 | "\n", 72 | "Pedersen commitments enable us to commit to scalar values on an elliptic curve.\n", 73 | "\n", 74 | "The maximum amount of information inside a value depends on the size of the curve.\n", 75 | "\n", 76 | "We commit to the scalar $v$ by creating the curve point $\\text{Com}(v) = G * v$.\n", 77 | "\n", 78 | "Because the discrete logarithm is hard, it is impossible to extract $v$ from $\\text{Com}(v)$." 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "id": "c1a0e09e", 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "def pedersen_attempt(v: Scalar) -> CurvePoint:\n", 89 | " return ONE_POINT * v\n", 90 | "\n", 91 | "v = Scalar.random()\n", 92 | "c1 = pedersen_attempt(v)\n", 93 | "print(f\"Commitment {c1} opens to value {v}\")\n", 94 | "\n", 95 | "c2 = pedersen_attempt(v)\n", 96 | "print(f\"Commitment {c2} also opens to value {v}\")" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "id": "6cc46ef1", 102 | "metadata": {}, 103 | "source": [ 104 | "# Pedersen commitment (hiding)\n", 105 | "\n", 106 | "There is a problem: Committing to the same value yields the same commitment. This reveals something about the contained value!\n", 107 | "\n", 108 | "As a fix, we introduce a **random blinding factor** $r$.\n", 109 | "\n", 110 | "To commit to the scalar $v$, we create the curve point $\\text{Com}(v, r) = G * v + H * r$ where $r$ is random.\n", 111 | "\n", 112 | "$H$ is a curve point that is known by all parties.\n", 113 | "\n", 114 | "This resulting Pedersen commitment is **hiding**." 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": null, 120 | "id": "61726a31", 121 | "metadata": {}, 122 | "outputs": [], 123 | "source": [ 124 | "h = Scalar(2)\n", 125 | "H = ONE_POINT * h\n", 126 | "\n", 127 | "def pedersen_hiding(v: Scalar) -> CurvePoint:\n", 128 | " r = Scalar.random()\n", 129 | " return ONE_POINT * v + H * r\n", 130 | "\n", 131 | "v = Scalar.random()\n", 132 | "c1 = pedersen_hiding(v)\n", 133 | "print(f\"Commitment {c1} opens to value {v}\")\n", 134 | "\n", 135 | "c2 = pedersen_hiding(v)\n", 136 | "print(f\"Commitment {c2} also opens to value {v}\")" 137 | ] 138 | }, 139 | { 140 | "cell_type": "markdown", 141 | "id": "6de487a5", 142 | "metadata": {}, 143 | "source": [ 144 | "# Hashing onto the curve\n", 145 | "\n", 146 | "We can create curve points with random xy coordinate:\n", 147 | "\n", 148 | "1. Generate random xy\n", 149 | "2. Return point if curve equation is satisfied\n", 150 | "3. Goto 1. otherwise\n", 151 | "\n", 152 | "This is called **hashing onto the curve** because the random xy coordinates come from hash functions.\n", 153 | "\n", 154 | "The resulting point $H$ is interesting because it is a **random point with unknown discrete logarithm!**\n", 155 | "\n", 156 | "From our viewpoint, $H$ is independent from the one-point $G$. **No one knows** any linear relation between $G$ and $H$.\n", 157 | "\n", 158 | "# Alternative one-points\n", 159 | "\n", 160 | "Think of $H$ as a one-point in \"another language\".\n", 161 | "\n", 162 | "We can iterate over the entire curve using $H$, but the order of points is completely different than for $G$.\n", 163 | "\n", 164 | "We can iterate: $G * 1 = (4, 2)$, $G * 2 = (3, 3)$, $G * 3 = (1, 2)$, $\\ldots$\n", 165 | "\n", 166 | "Or we can iterate: $H * 1 = (2, 5)$, $H * 2 = (5, 4)$, $H * 3 = (4, 5)$, $\\ldots$\n", 167 | "\n", 168 | "The concrete coordinates depend on the curve and $G$ and $H$." 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": null, 174 | "id": "5943b943", 175 | "metadata": {}, 176 | "outputs": [], 177 | "source": [ 178 | "puncto_uno, = CurvePoint.sample_greater_one(1)\n", 179 | "\n", 180 | "print(f\"G * 1 = {ONE_POINT * Scalar(1)}, G * 2 = {ONE_POINT * Scalar(2)}, G * 3 = {ONE_POINT * Scalar(3)}, ...\")\n", 181 | "print(f\"H * 1 = {puncto_uno * Scalar(1)}, H * 2 = {puncto_uno * Scalar(2)}, H * 3 = {puncto_uno * Scalar(3)}, ...\")" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "id": "69931d2e", 187 | "metadata": {}, 188 | "source": [ 189 | "# Not binding\n", 190 | "\n", 191 | "There is another problem: Peggy knows the blinding factor and the discrete logarithm of $H$:\n", 192 | "\n", 193 | "$G * v + H * r = G * v + (G * h) * r$\n", 194 | "\n", 195 | "She can open the same commitment to different values!\n", 196 | "\n", 197 | "$\\text{Com}(v, r) = G * v + (G * h) * r = G * (h * r) + (G * h) * \\frac{v}{h} = \\text{Com}(rh, \\frac{v}{h})$\n", 198 | "\n", 199 | "Here, Peggy changed the commitment from $v$ to $rh$. Many other shenanigans is possible." 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": null, 205 | "id": "cd6a0b74", 206 | "metadata": {}, 207 | "outputs": [], 208 | "source": [ 209 | "# Rerun this a couple of times\n", 210 | "\n", 211 | "v = Scalar.random()\n", 212 | "r = Scalar.random()\n", 213 | "c1 = ONE_POINT * v + H * r\n", 214 | "print(f\"Commitment {c1} opens to value {v}\")\n", 215 | "\n", 216 | "hr = h * r\n", 217 | "vh = v / h\n", 218 | "c2 = ONE_POINT * hr + H * vh\n", 219 | "print(f\"Commitment {c2} opens to value {hr}\")\n", 220 | "\n", 221 | "assert c1 == c2" 222 | ] 223 | }, 224 | { 225 | "cell_type": "markdown", 226 | "id": "70299aba", 227 | "metadata": {}, 228 | "source": [ 229 | "# Pedersen commitment (hiding and binding)\n", 230 | "\n", 231 | "As a fix, we require that $H$ has an **unknown discrete logarithm**.\n", 232 | "\n", 233 | "We cannot repeat our tricks from earlier.\n", 234 | "\n", 235 | "To commit to the scalar $v$, we create the curve point $\\text{Com}(v, r) = G * v + H * r$ where $r$ is random.\n", 236 | "\n", 237 | "This resulting Pedersen commitment is hiding and **binding**." 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": null, 243 | "id": "0f5f9905", 244 | "metadata": {}, 245 | "outputs": [], 246 | "source": [ 247 | "puncto_uno, = CurvePoint.sample_greater_one(1)\n", 248 | "\n", 249 | "def pedersen_binding(v: Scalar) -> CurvePoint:\n", 250 | " r = Scalar.random()\n", 251 | " return ONE_POINT * v + puncto_uno * r\n", 252 | "\n", 253 | "v = Scalar.random()\n", 254 | "c1 = pedersen_binding(v)\n", 255 | "print(f\"Commitment {c1} opens to value {v}\")\n", 256 | "\n", 257 | "c2 = pedersen_binding(v)\n", 258 | "print(f\"Commitment {c2} also opens to value {v}\")" 259 | ] 260 | }, 261 | { 262 | "cell_type": "markdown", 263 | "id": "9de3ad41", 264 | "metadata": {}, 265 | "source": [ 266 | "# Pedersen commitment (multiple values)\n", 267 | "\n", 268 | "We can commit to a **list of values** by using a **list of independent one-points**: $G_0$, $G_1$, $\\ldots$, $G_n$, $H$\n", 269 | "\n", 270 | "One of these can be the normal one-point and the rest must have an unknown discrete logarithm.\n", 271 | "\n", 272 | "To commit to values $v_0$, $v_1$, $\\ldots$, $v_n$, we create the **single curve point** $\\text{Com}(v_0, v_1, \\ldots, v_n; r) = G_0 * v_0 + G_1 * v_1 + \\ldots + G_n * v_n + H * r$ where $r$ is random.\n", 273 | "\n", 274 | "Notice how the list of values is compressed inside a single curve point! The commitment is extremely space-efficient (constant size). Opening the commitment requires revealing the list of values and the blinding factor (linear size).\n", 275 | "\n", 276 | "The commitment is **hiding** because of the blinding factor.\n", 277 | "\n", 278 | "The commitment is **binding** because we don't know the linear relations between the used one-points. Peggy is forced to use the same list of values plus blinding factor to restore the sum point that is the commitment." 279 | ] 280 | }, 281 | { 282 | "cell_type": "code", 283 | "execution_count": null, 284 | "id": "00049075", 285 | "metadata": {}, 286 | "outputs": [], 287 | "source": [ 288 | "puncto_uno, premier_point, daiichi, = CurvePoint.sample_greater_one(3)\n", 289 | "\n", 290 | "def pedersen_multiple(v0: Scalar, v1: Scalar, v2: Scalar) -> CurvePoint:\n", 291 | " r = Scalar.random()\n", 292 | " return ONE_POINT * r + puncto_uno * v0 + premier_point * v1 + daiichi * v2\n", 293 | "\n", 294 | "v0, v1, v2 = Scalar.random(), Scalar.random(), Scalar.random()\n", 295 | "c1 = pedersen_multiple(v0, v1, v2)\n", 296 | "print(f\"Commitment {c1} opens to values {v0}, {v1}, {v2}\")\n", 297 | "\n", 298 | "c2 = pedersen_multiple(v0, v1, v2)\n", 299 | "print(f\"Commitment {c2} also opens to values {v0}, {v1}, {v2}\")" 300 | ] 301 | }, 302 | { 303 | "cell_type": "code", 304 | "execution_count": null, 305 | "id": "dd33f7f5", 306 | "metadata": {}, 307 | "outputs": [], 308 | "source": [] 309 | } 310 | ], 311 | "metadata": { 312 | "kernelspec": { 313 | "display_name": "Python 3 (ipykernel)", 314 | "language": "python", 315 | "name": "python3" 316 | }, 317 | "language_info": { 318 | "codemirror_mode": { 319 | "name": "ipython", 320 | "version": 3 321 | }, 322 | "file_extension": ".py", 323 | "mimetype": "text/x-python", 324 | "name": "python", 325 | "nbconvert_exporter": "python", 326 | "pygments_lexer": "ipython3", 327 | "version": "3.10.12" 328 | } 329 | }, 330 | "nbformat": 4, 331 | "nbformat_minor": 5 332 | } 333 | -------------------------------------------------------------------------------- /play_sudoku.py: -------------------------------------------------------------------------------- 1 | """ 2 | Play as Victor in an interactive zero-knowledge proof of a Sudoku solution. 3 | 4 | See the argparse description for more. 5 | 6 | The inspiration for this game was another interactive Sudoku prover: https://manishearth.github.io/sudoku-zkp/zkp.html 7 | """ 8 | 9 | import argparse 10 | import logging 11 | import random 12 | 13 | import pygame 14 | import sys 15 | from typing import Tuple 16 | from local.sudoku import Board 17 | from local.graph import Mapping 18 | 19 | WHITE = (255, 255, 255) 20 | BLACK = (0, 0, 0) 21 | RED = (255, 0, 0) 22 | GREEN = (50, 205, 50) 23 | BLUE = (0, 0, 255) 24 | 25 | 26 | def draw_grid(): 27 | """ 28 | Draw the Sudoku grid (without values). 29 | """ 30 | # Columns 31 | for x in range(board_x_min, board_x_max, cell_visual_width): 32 | pygame.draw.line(window, BLACK, (x, board_y_min), (x, board_y_max)) 33 | # Rows 34 | for y in range(board_y_min, board_y_max, cell_visual_width): 35 | pygame.draw.line(window, BLACK, (board_x_min, y), (board_x_max, y)) 36 | # Boxes 37 | for x in range(board_x_min, board_x_max, cell_visual_width * dim): 38 | pygame.draw.line(window, BLACK, (x, board_y_min), (x, board_y_max), width=3) 39 | for y in range(board_y_min, board_y_max, cell_visual_width * dim): 40 | pygame.draw.line(window, BLACK, (board_x_min, y), (board_x_max, y), width=3) 41 | 42 | 43 | def draw_public_solution(): 44 | """ 45 | Draw the value of the public solution. 46 | """ 47 | for row in range(dim_sq): 48 | for col in range(dim_sq): 49 | value = public_solution[row][col] 50 | if value != 0: 51 | cell_text = font.render(str(value), True, BLACK) 52 | cell_x_min = board_x_min + col * cell_visual_width 53 | cell_y_min = board_y_min + row * cell_visual_width 54 | cell_rect = cell_text.get_rect(center=(cell_x_min + cell_visual_width // 2, cell_y_min + cell_visual_width // 2)) 55 | window.blit(cell_text, cell_rect) 56 | 57 | 58 | def inside_board(x: int, y: int) -> bool: 59 | """ 60 | Check whether the coordinates are inside the board. 61 | 62 | :param x: x coordinate 63 | :param y: y coordinate 64 | :return: is inside the board 65 | """ 66 | return board_x_min <= x < board_x_max and board_y_min <= y < board_y_max 67 | 68 | 69 | def inside_accept(x: int, y: int) -> bool: 70 | """ 71 | Check whether the coordinates are inside the accept button. 72 | 73 | :param x: x coordinate 74 | :param y: y coordinate 75 | :return: is inside the accept button 76 | """ 77 | return accept_x_min <= x < accept_x_max and accept_y_min <= y < accept_y_max 78 | 79 | 80 | def inside_reject(x: int, y: int) -> bool: 81 | """ 82 | Check whether the coordinates are inside the reject button 83 | 84 | :param x: x coordinate 85 | :param y: y coordinate 86 | :return: is inside the reject button 87 | """ 88 | return reject_x_min <= x < reject_x_max and reject_y_min <= y < reject_y_max 89 | 90 | 91 | def get_selection(x: int, y: int, mode: str) -> Tuple[int, int]: 92 | """ 93 | Return the area that the player selected. 94 | 95 | :param x: selected x coordinate 96 | :param y: selected y coordinate 97 | :param mode: row, column, box, preset 98 | :return: selected row and selected column 99 | """ 100 | row, col = (y - board_y_min) // cell_visual_width, (x - board_x_min) // cell_visual_width 101 | 102 | if mode == "row" or mode == "column": 103 | return row, col 104 | elif mode == "box": 105 | box_row = (row // dim) * dim 106 | box_col = (col // dim) * dim 107 | return box_row, box_col 108 | elif mode == "presets": 109 | return 0, 0 # Default 110 | else: 111 | raise ValueError(f"Unknown selection mode: {mode}") 112 | 113 | 114 | def highlight_selection(row: int, col: int, mode: str): 115 | """ 116 | Highlight the area that the player selected. 117 | 118 | :param row: selected row 119 | :param col: selected column 120 | :param mode: row, column, box, presets 121 | """ 122 | if mode == "row": 123 | x0 = board_x_min 124 | y0 = board_y_min + row * cell_visual_width 125 | x_offset = board_visual_width 126 | y_offset = cell_visual_width 127 | elif mode == "column": 128 | x0 = board_x_min + col * cell_visual_width 129 | y0 = board_y_min 130 | x_offset = cell_visual_width 131 | y_offset = board_visual_width 132 | elif mode == "box": 133 | start_row, start_col = (row // dim) * dim, (col // dim) * dim 134 | x0 = board_x_min + start_col * cell_visual_width 135 | y0 = board_y_min + start_row * cell_visual_width 136 | x_offset = cell_visual_width * dim 137 | y_offset = cell_visual_width * dim 138 | elif mode == "presets": 139 | x0 = board_x_min 140 | y0 = board_y_min 141 | x_offset = board_visual_width 142 | y_offset = board_visual_width 143 | else: 144 | raise ValueError(f"Unknown selection mode: {mode}") 145 | 146 | pygame.draw.rect(window, RED, (x0, y0, x_offset, y_offset)) 147 | 148 | 149 | def reveal_partial_solution(row: int, col: int, mode: str): 150 | """ 151 | Update the public solution based on the area that the player selected. 152 | 153 | The values in the secret solution are randomly shuffled (permuted). 154 | 155 | The selected area is revealed while everything else is hidden. 156 | 157 | :param col: selected column 158 | :param row: selected row 159 | :param mode: row, column, box, presents 160 | """ 161 | global public_solution 162 | secret_mapping = Mapping.shuffle_list(list(range(1, dim_sq + 1))) 163 | logging.debug(f"Secret mapping {secret_mapping}") 164 | public_solution = Board.blank(dim) 165 | 166 | if mode == "row": 167 | for col in range(dim_sq): 168 | public_solution[row][col] = secret_mapping[secret_solution[row][col]] 169 | elif mode == "column": 170 | for row in range(dim_sq): 171 | public_solution[row][col] = secret_mapping[secret_solution[row][col]] 172 | elif mode == "box": 173 | for row_offset in range(dim): 174 | for col_offset in range(dim): 175 | public_solution[row + row_offset][col + col_offset] = secret_mapping[secret_solution[row + row_offset][col + col_offset]] 176 | elif mode == "presets": 177 | for row in range(dim_sq): 178 | for col in range(dim_sq): 179 | if public_presets[row][col] > 0: 180 | public_solution[row][col] = secret_mapping[secret_solution[row][col]] 181 | 182 | 183 | def verify_partial_solution(row: int, col: int, mode: str) -> bool: 184 | """ 185 | Verify that the revealed partial solution is valid. 186 | 187 | :param row: selected row 188 | :param col: selected column 189 | :param mode: row, column, box, presents 190 | :return: partial solution is valid 191 | """ 192 | if mode == "row": 193 | columns = [public_solution[row][col] for col in range(dim_sq)] 194 | logging.info(f"Checking row {columns}") 195 | return public_solution.verify_area(columns) 196 | elif mode == "column": 197 | rows = [public_solution[row][col] for row in range(dim_sq)] 198 | logging.info(f"Checking column {rows}") 199 | return public_solution.verify_area(rows) 200 | elif mode == "box": 201 | box = [public_solution[row + row_offset][col + col_offset] for row_offset in range(dim) for col_offset in range(dim)] 202 | logging.info(f"Checking box {box}") 203 | return public_solution.verify_area(box) 204 | elif mode == "presets": 205 | shuffled_values = [public_solution[row][col] for row in range(dim_sq) for col in range(dim_sq) if public_solution[row][col] > 0] 206 | return public_presets.verify_shuffling(iter(shuffled_values)) 207 | else: 208 | raise ValueError(f"Unknown selection mode: {mode}") 209 | 210 | 211 | def player_wins(accept: bool) -> bool: 212 | """ 213 | Check whether the player wins. 214 | 215 | The player wins by accepting a correct solution or by rejecting a false solution. 216 | The player loses otherwise (accepting a false solution or rejecting a correct one). 217 | 218 | :param accept: player accepted the solution 219 | :return: player wins 220 | """ 221 | return accept == honest 222 | 223 | 224 | def run_game(): 225 | """ 226 | Run the game loop until the player quits. 227 | 228 | Requires the global variables to be set beforehand for configuration. 229 | """ 230 | round_number = 1 231 | mode = "row" 232 | game_state = "playing" 233 | 234 | while True: 235 | window.fill(WHITE) 236 | x, y = pygame.mouse.get_pos() 237 | row, col = get_selection(x, y, mode) 238 | 239 | for event in pygame.event.get(): 240 | # Always allow player to quit 241 | if event.type == pygame.QUIT: 242 | pygame.quit() 243 | sys.exit() 244 | # Disable other controls when game is over 245 | if game_state == "win" or game_state == "lose": 246 | continue 247 | if event.type == pygame.KEYDOWN: 248 | if event.key == pygame.K_1: 249 | mode = "row" 250 | elif event.key == pygame.K_2: 251 | mode = "column" 252 | elif event.key == pygame.K_3: 253 | mode = "box" 254 | elif event.key == pygame.K_4: 255 | mode = "presets" 256 | elif event.type == pygame.MOUSEBUTTONDOWN: 257 | if inside_accept(x, y) or inside_reject(x, y): 258 | if inside_accept(x, y): 259 | accept = True 260 | else: 261 | accept = False 262 | 263 | if player_wins(accept): 264 | game_state = "win" 265 | else: 266 | game_state = "lose" 267 | elif inside_board(x, y): 268 | reveal_partial_solution(row, col, mode) 269 | 270 | if not verify_partial_solution(row, col, mode): 271 | game_state = "win" 272 | else: 273 | round_number += 1 274 | 275 | if game_state == "win": 276 | text = font.render(f"You win! You took {round_number} rounds.", True, BLUE) 277 | text_rect = text.get_rect(center=(screen_x_max // 2, screen_y_max // 2)) 278 | window.blit(text, text_rect) 279 | elif game_state == "lose": 280 | text = font.render(f"You lose! You took {round_number} rounds.", True, RED) 281 | text_rect = text.get_rect(center=(screen_x_max // 2, screen_y_max // 2)) 282 | window.blit(text, text_rect) 283 | else: 284 | # Board 285 | if inside_board(x, y): 286 | highlight_selection(row, col, mode) 287 | 288 | draw_grid() 289 | draw_public_solution() 290 | 291 | # Round text 292 | round_text = font.render(f"Round: {round_number}", True, BLACK) 293 | round_rect = round_text.get_rect(left=text_margin, centery=board_y_min // 2) 294 | window.blit(round_text, round_rect) 295 | 296 | # Accept button 297 | pygame.draw.rect(window, GREEN, (accept_x_min, accept_y_min, button_x_length, button_y_length)) 298 | accept_text = font.render("Accept", True, BLACK) 299 | accept_rect = accept_text.get_rect(left=accept_x_min + text_margin, centery=board_y_min // 2) 300 | window.blit(accept_text, accept_rect) 301 | 302 | # Reject button 303 | pygame.draw.rect(window, RED, (reject_x_min, reject_y_min, button_x_length, button_y_length)) 304 | reject_text = font.render("Reject", True, BLACK) 305 | reject_rect = reject_text.get_rect(left=reject_x_min + text_margin, centery=board_y_min // 2) 306 | window.blit(reject_text, reject_rect) 307 | 308 | pygame.display.flip() 309 | clock.tick(60) 310 | 311 | 312 | if __name__ == "__main__": 313 | parser = argparse.ArgumentParser( 314 | description="Interactive zero-knowledge proof of a Sudoku solution.", 315 | epilog=""" 316 | Play the side of Victor. 317 | Does Peggy know a valid solution? Be warned, she might be lying! 318 | Check a few rows, columns, boxes or the presets to increase your confidence that she is honest (use keys 1 to 4 plus mouse). 319 | You win immediately if Peggy reveals an inconsistent solution. 320 | Accept or reject when you feel ready. Were you correct? How many rounds did you take? 321 | You win if you made the correct decision. Otherwise you lose. 322 | Try to use as few rounds as possible. 323 | """ 324 | ) 325 | parser.add_argument("--dim", type=int, default=3, 326 | help="Base size of the Sudoku puzzle: number of cells alongside a box") 327 | parser.add_argument("--debug", action="store_true", help="Show debug information (secret information)") 328 | args = parser.parse_args() 329 | 330 | if args.debug: 331 | logging.basicConfig(level=logging.DEBUG) 332 | else: 333 | logging.basicConfig(level=logging.INFO) 334 | 335 | # Sudoku problem 336 | 337 | dim = args.dim 338 | dim_sq = dim ** 2 339 | 340 | honest = random.choice([True, False]) 341 | secret_solution = Board.random(dim).solve() 342 | 343 | if not honest: 344 | secret_solution.falsify(1) 345 | 346 | public_presets = secret_solution.to_puzzle() 347 | public_solution = public_presets # Presets stay constant while the (partial) solution keeps changing 348 | 349 | # Visual board 350 | 351 | cell_visual_width = 50 352 | board_visual_width = cell_visual_width * dim_sq 353 | 354 | board_x_min = 0 355 | board_x_max = board_x_min + board_visual_width 356 | board_y_min = 50 357 | board_y_max = board_y_min + board_visual_width 358 | 359 | screen_x_max = board_x_max 360 | screen_y_max = board_y_max 361 | 362 | # Visual buttons 363 | 364 | text_margin = 10 365 | button_x_length = 100 366 | button_y_length = 40 367 | 368 | accept_x_min = screen_x_max - button_x_length - text_margin - button_x_length - text_margin 369 | accept_y_min = board_y_min // 2 - button_y_length // 2 370 | accept_x_max = accept_x_min + button_x_length 371 | accept_y_max = accept_y_min + button_y_length 372 | 373 | reject_x_min = screen_x_max - button_x_length - text_margin 374 | reject_y_min = board_y_min // 2 - button_y_length // 2 375 | reject_x_max = reject_x_min + button_x_length 376 | reject_y_max = reject_y_min + button_y_length 377 | 378 | # Pygame initialization 379 | 380 | pygame.init() 381 | window = pygame.display.set_mode((screen_x_max, screen_y_max)) 382 | pygame.display.set_caption("Sudoku") 383 | font = pygame.font.Font(None, 36) 384 | clock = pygame.time.Clock() 385 | 386 | run_game() 387 | -------------------------------------------------------------------------------- /graph/non_isomorphism.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "4bb4afd6", 6 | "metadata": {}, 7 | "source": [ 8 | "# Graph non-isomorphism\n", 9 | "\n", 10 | "In this chapter we construct a zero-knowledge protocol around graph non-isomorphism.\n", 11 | "\n", 12 | "This chapter is based on [a lecture from the Max Plank Institute for Informatics](https://resources.mpi-inf.mpg.de/departments/d1/teaching/ss13/gitcs/lecture9.pdf)." 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "7a27c0ac", 18 | "metadata": {}, 19 | "source": [ 20 | "# What is a graph?\n", 21 | "\n", 22 | "[A graph](https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)) consists of nodes and edges. Nodes are points in space. Edges are bridges between nodes.\n", 23 | "\n", 24 | "# What is an isomorphism?\n", 25 | "\n", 26 | "[Two graphs are isomorphic](https://en.wikipedia.org/wiki/Graph_isomorphism) if they have the same structure. By changing the names of the nodes of the first graph, we can obtain the second graph, and vice versa. There exists a translation of node names.\n", 27 | "\n", 28 | "Given two large random graphs, it is hard to know if they are isomorphic. There is no known algorithm to efficiently compute this (in polynomial time)." 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "id": "34811593", 34 | "metadata": {}, 35 | "source": [ 36 | "# What are we proving?\n", 37 | "\n", 38 | "Peggy and Victor are engaged in an interactive proof.\n", 39 | "\n", 40 | "There are two graphs.\n", 41 | "\n", 42 | "Peggy thinks she can differentiate between both graphs (both graphs are non-isomorphic). She wants to prove that to Victor.\n", 43 | "\n", 44 | "Victor is sceptical and wants to see evidence. He wants to expose Peggy as a liar if both graphs are isomorphic.\n", 45 | "\n", 46 | "Peggy wins if she convinces Victor. Victor wins by accepting only graphs that are structually different." 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "id": "d1b79ce1", 52 | "metadata": {}, 53 | "source": [ 54 | "# Set up Jupyter\n", 55 | "\n", 56 | "Run the following snippet to set up your Jupyter notebook for the workshop." 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": null, 62 | "id": "e6b8a39a", 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "import os\n", 67 | "import sys\n", 68 | "\n", 69 | "# Add project root so we can import local modules\n", 70 | "root_dir = sys.path.append(\"..\")\n", 71 | "sys.path.append(root_dir)\n", 72 | "\n", 73 | "# Import here so cells don't depend on each other\n", 74 | "from IPython.display import display\n", 75 | "from typing import List, Tuple, Dict\n", 76 | "import ipywidgets as widgets\n", 77 | "import random\n", 78 | "import networkx as nx\n", 79 | "import matplotlib.pyplot as plt\n", 80 | "\n", 81 | "from local.graph import Mapping, random_graph, non_isomorphic_graph\n", 82 | "import local.stats as stats" 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "id": "8f903c4c", 88 | "metadata": {}, 89 | "source": [ 90 | "# Select the scenario\n", 91 | "\n", 92 | "Choose the good or the evil scenario. See how it affects the other cells further down.\n", 93 | "\n", 94 | "1. **Peggy is honest** 😇 She knows a way to differentiate both graphs. She wants to convince Victor of a true statement.\n", 95 | "2. **Peggy is lying** 😈 Both graphs actually look the same to her! She tries to fool Victor into believing a false statement.\n", 96 | "\n", 97 | "Also select the **size of the graphs**." 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "id": "450b4872", 104 | "metadata": {}, 105 | "outputs": [], 106 | "source": [ 107 | "def generate_graphs(values: Dict):\n", 108 | " global graph1, graph2, from_1_to_2\n", 109 | " \n", 110 | " n_edges = n_nodes_slider.value\n", 111 | " graph1 = random_graph(n_nodes_slider.value, n_edges)\n", 112 | "\n", 113 | " if honest_dropdown.value:\n", 114 | " # Good: Both graphs are different\n", 115 | " graph2 = non_isomorphic_graph(graph1)\n", 116 | " else:\n", 117 | " # Evil: Both graphs are isomorphic\n", 118 | " from_1_to_2 = Mapping.shuffle_graph(graph1)\n", 119 | " graph2 = from_1_to_2.apply_graph(graph1)\n", 120 | "\n", 121 | "honest_dropdown = widgets.Dropdown(\n", 122 | " options=[\n", 123 | " (\"Peggy can differentiate 😇\", True),\n", 124 | " (\"Peggy cannot differentiate 😈\", False)],\n", 125 | " value=True,\n", 126 | " description=\"Scenario:\",\n", 127 | ")\n", 128 | "honest_dropdown.observe(generate_graphs, names=\"value\")\n", 129 | "\n", 130 | "n_nodes_slider = widgets.IntSlider(min=4, max=20, value=4, step=1, description=\"#Nodes\")\n", 131 | "n_nodes_slider.observe(generate_graphs, names=\"value\")\n", 132 | "\n", 133 | "# Generate default values\n", 134 | "generate_graphs({})\n", 135 | "# Display selection\n", 136 | "display(honest_dropdown)\n", 137 | "display(n_nodes_slider)" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "id": "b204164c", 143 | "metadata": {}, 144 | "source": [ 145 | "# Visualize your graphs\n", 146 | "\n", 147 | "Visualize the graphs you generated." 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": null, 153 | "id": "55793440", 154 | "metadata": {}, 155 | "outputs": [], 156 | "source": [ 157 | "print(\"Graph 1\")\n", 158 | "nx.draw(graph1, with_labels=True)\n", 159 | "plt.show()\n", 160 | "\n", 161 | "print(\"Graph 2\")\n", 162 | "nx.draw(graph2, with_labels=True)\n", 163 | "plt.show()" 164 | ] 165 | }, 166 | { 167 | "cell_type": "markdown", 168 | "id": "2a76c669", 169 | "metadata": {}, 170 | "source": [ 171 | "# How the proof goes\n", 172 | "\n", 173 | "1. Victor randomly chooses graph 1 or 2 and shuffles it, to obtain graph $S$.\n", 174 | "1. Victor sends $S$ to Peggy.\n", 175 | "1. Peggy decides if $S$ came from graph 1 or 2 and sends her answer to Victor.\n", 176 | "1. Victor checks if Peggy answered correctly." 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": null, 182 | "id": "23b6d019", 183 | "metadata": { 184 | "scrolled": true 185 | }, 186 | "outputs": [], 187 | "source": [ 188 | "class Peggy:\n", 189 | " def __init__(self, graph1: nx.Graph, graph2: nx.Graph):\n", 190 | " self.graph1 = graph1\n", 191 | " self.graph2 = graph2\n", 192 | " \n", 193 | " def distinguish(self, shuffled_graph: nx.Graph) -> nx.Graph:\n", 194 | " if nx.is_isomorphic(self.graph1, shuffled_graph):\n", 195 | " return 0\n", 196 | " else:\n", 197 | " assert nx.is_isomorphic(self.graph2, shuffled_graph)\n", 198 | " return 1\n", 199 | "\n", 200 | "\n", 201 | "class Victor:\n", 202 | " def __init__(self, graph1: nx.Graph, graph2: nx.Graph):\n", 203 | " self.graphs = [graph1, graph2]\n", 204 | " \n", 205 | " def shuffled_graph(self) -> nx.Graph:\n", 206 | " self.chosen_index = random.randrange(0, 2)\n", 207 | " chosen_graph = self.graphs[self.chosen_index]\n", 208 | " shuffle = Mapping.shuffle_graph(chosen_graph)\n", 209 | " shuffled_graph = shuffle.apply_graph(chosen_graph)\n", 210 | " \n", 211 | " return shuffled_graph\n", 212 | " \n", 213 | " def verify(self, index: int) -> bool:\n", 214 | " return index == self.chosen_index" 215 | ] 216 | }, 217 | { 218 | "cell_type": "markdown", 219 | "id": "31bac646", 220 | "metadata": {}, 221 | "source": [ 222 | "# Run the proof\n", 223 | "\n", 224 | "Let's see the proof in action.\n", 225 | "\n", 226 | "Run the Python code below and see what happens.\n", 227 | "\n", 228 | "The outcome depends on the scenario you picked. The outcome is also randomly different each time.\n", 229 | "\n", 230 | "Feel free to run the code multiple times!" 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": null, 236 | "id": "7c1bca4f", 237 | "metadata": {}, 238 | "outputs": [], 239 | "source": [ 240 | "peggy = Peggy(graph1, graph2)\n", 241 | "victor = Victor(graph1, graph2)\n", 242 | "\n", 243 | "shuffled_graph = victor.shuffled_graph()\n", 244 | "index = peggy.distinguish(shuffled_graph)\n", 245 | "\n", 246 | "if victor.verify(index):\n", 247 | " if honest_dropdown.value:\n", 248 | " print(\"Victor is convinced 👌 (expected)\")\n", 249 | " else:\n", 250 | " print(\"Victor is convinced 👌 (Victor was fooled)\")\n", 251 | "else:\n", 252 | " if honest_dropdown.value:\n", 253 | " print(\"Victor is not convinced... 🤨 (Peggy was dumb)\")\n", 254 | " else:\n", 255 | " print(\"Victor is not convinced... 🤨 (expected)\")" 256 | ] 257 | }, 258 | { 259 | "cell_type": "markdown", 260 | "id": "d5137a1f", 261 | "metadata": {}, 262 | "source": [ 263 | "# How the proof is complete\n", 264 | "\n", 265 | "If Peggy can differentiate between both graphs, then **Victor will always be convinced** by her proof.\n", 266 | "\n", 267 | "This is because Peggy is always able to answer which graph was shuffled.\n", 268 | "\n", 269 | "Let's run a couple of exchanges and see how they go." 270 | ] 271 | }, 272 | { 273 | "cell_type": "code", 274 | "execution_count": null, 275 | "id": "71bb32b0", 276 | "metadata": {}, 277 | "outputs": [], 278 | "source": [ 279 | "n_exchanges_complete_slider = widgets.IntSlider(min=10, max=1000, value=10, step=10, description=\"#Exchanges\")\n", 280 | "n_exchanges_complete_slider" 281 | ] 282 | }, 283 | { 284 | "cell_type": "code", 285 | "execution_count": null, 286 | "id": "60792496", 287 | "metadata": {}, 288 | "outputs": [], 289 | "source": [ 290 | "# Good scenario:\n", 291 | "# Both graphs are different\n", 292 | "graph3 = non_isomorphic_graph(graph1)\n", 293 | "\n", 294 | "honest_peggy = Peggy(graph1, graph3)\n", 295 | "victor = Victor(graph1, graph3)\n", 296 | "\n", 297 | "peggy_success = 0\n", 298 | "\n", 299 | "for _ in range(n_exchanges_complete_slider.value):\n", 300 | " shuffled_graph = victor.shuffled_graph()\n", 301 | " index = honest_peggy.distinguish(shuffled_graph)\n", 302 | "\n", 303 | " if victor.verify(index):\n", 304 | " peggy_success += 1\n", 305 | " \n", 306 | "peggy_success_rate = peggy_success / n_exchanges_complete_slider.value * 100\n", 307 | "\n", 308 | "print(f\"Running {n_exchanges_complete_slider.value} exchanges.\")\n", 309 | "print(f\"Honest Peggy wins {peggy_success_rate:0.2f}% of the time.\")\n", 310 | "print()\n", 311 | "\n", 312 | "assert peggy_success_rate == 100\n", 313 | "print(\"Peggy always wins if she is honest.\")" 314 | ] 315 | }, 316 | { 317 | "cell_type": "markdown", 318 | "id": "562c15df", 319 | "metadata": {}, 320 | "source": [ 321 | "# How the proof is sound\n", 322 | "\n", 323 | "If Peggy cannot differentiate both graphs, then **Victor has a chance to reject** her proof.\n", 324 | "\n", 325 | "Because there are two graphs, Peggy has a 50% chance to randomly guess the graph that Victor shuffled. This is not great.\n", 326 | "\n", 327 | "We can increase Victor's confidence by running the protocol for **multiple rounds**. This means Victor randomly selects and shuffles multiple times and Peggy has to answer which graph he shuffled. Victor accepts if Peggy answered correctly **all** time times. However, he rejects if Peggy answers incorrectly **even once**.\n", 328 | "\n", 329 | "The chance that Peggy randomly guesses correctly for $n$ rounds is $\\left(\\frac{1}{2}\\right)^n$, which decreases exponentially in $n$. This is tiny! If Peggy answers correctly, then Victor is confident that she didn't cheat.\n", 330 | "\n", 331 | "Let's run a couple of exchanges and see how they go." 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": null, 337 | "id": "34f80fd2", 338 | "metadata": {}, 339 | "outputs": [], 340 | "source": [ 341 | "n_exchanges_sound_slider = widgets.IntSlider(min=10, max=1000, value=10, step=10, description=\"#Exchanges\")\n", 342 | "n_rounds_slider = widgets.IntSlider(min=1, max=10, value=1, step=1, description=\"#Rounds\")\n", 343 | "\n", 344 | "display(n_exchanges_sound_slider)\n", 345 | "display(n_rounds_slider)" 346 | ] 347 | }, 348 | { 349 | "cell_type": "code", 350 | "execution_count": null, 351 | "id": "fa919e3b", 352 | "metadata": {}, 353 | "outputs": [], 354 | "source": [ 355 | "# Evil scenario:\n", 356 | "# Both graphs are isomorphic\n", 357 | "from_1_to_4 = Mapping.shuffle_graph(graph1)\n", 358 | "graph4 = from_1_to_4.apply_graph(graph1)\n", 359 | "\n", 360 | "lying_peggy = Peggy(graph1, graph4)\n", 361 | "victor = Victor(graph1, graph4)\n", 362 | "\n", 363 | "victor_success = 0\n", 364 | "\n", 365 | "for _ in range(n_exchanges_sound_slider.value):\n", 366 | " for _ in range(n_rounds_slider.value):\n", 367 | " shuffled_graph = victor.shuffled_graph()\n", 368 | " index = lying_peggy.distinguish(shuffled_graph)\n", 369 | " \n", 370 | " if not victor.verify(index):\n", 371 | " victor_success += 1\n", 372 | " break\n", 373 | " \n", 374 | "victor_success_rate = victor_success / n_exchanges_sound_slider.value * 100\n", 375 | "\n", 376 | "print(f\"Running {n_exchanges_sound_slider.value} exchanges with {n_rounds_slider.value} rounds each.\")\n", 377 | "print(f\"Victor wins against lying Peggy {victor_success_rate:0.2f}% of the time.\")\n", 378 | "print()\n", 379 | "\n", 380 | "if victor_success_rate < 50:\n", 381 | " print(\"Victor loses quite often for a small number of rounds.\")\n", 382 | "elif victor_success_rate < 90:\n", 383 | " print(\"Victor gains more confidence with each added round.\")\n", 384 | "else:\n", 385 | " print(\"At some point it is basically impossible to fool Victor.\")" 386 | ] 387 | }, 388 | { 389 | "cell_type": "markdown", 390 | "id": "f38dca91", 391 | "metadata": {}, 392 | "source": [ 393 | "# How the proof is zero-knowledge\n", 394 | "\n", 395 | "The proof itself looks like random noise. Nothing can be extracted from this noise.\n", 396 | "\n", 397 | "Everything that is sent over the wire is randomized:\n", 398 | "\n", 399 | "1. Victor sends a randomly shuffled graph.\n", 400 | "1. Peggy sends an index which depends on Victor's random choice.\n", 401 | "\n", 402 | "We can replicate this pattern:\n", 403 | "\n", 404 | "1. Compute a random index (0 or 1).\n", 405 | "1. Randomly shuffle the graph at the index.\n", 406 | "\n", 407 | "Let's run a chi-square test to see if the original transcripts are distinguishable from the fake transcripts.\n", 408 | "\n", 409 | "**Try small graphs first!** They require fewer samples than large graphs." 410 | ] 411 | }, 412 | { 413 | "cell_type": "code", 414 | "execution_count": null, 415 | "id": "8a7b9bb0", 416 | "metadata": {}, 417 | "outputs": [], 418 | "source": [ 419 | "n_transcripts_slider = widgets.IntSlider(min=1000, max=50000, value=10000, step=1000, description=\"#Transcripts\")\n", 420 | "n_transcripts_slider" 421 | ] 422 | }, 423 | { 424 | "cell_type": "code", 425 | "execution_count": null, 426 | "id": "d09b788b", 427 | "metadata": {}, 428 | "outputs": [], 429 | "source": [ 430 | "peggy = Peggy(graph1, graph2)\n", 431 | "victor = Victor(graph1, graph2)\n", 432 | "\n", 433 | "def real_transcript() -> Tuple:\n", 434 | " shuffled_graph = victor.shuffled_graph()\n", 435 | " index = peggy.distinguish(shuffled_graph)\n", 436 | " \n", 437 | " return tuple(shuffled_graph.edges()), index\n", 438 | "\n", 439 | "\n", 440 | "def fake_transcript() -> Tuple:\n", 441 | " index = random.randrange(0, 2)\n", 442 | " chosen_graph = [graph1, graph2][index]\n", 443 | " shuffle = Mapping.shuffle_graph(chosen_graph)\n", 444 | " shuffled_graph = shuffle.apply_graph(chosen_graph)\n", 445 | " \n", 446 | " return tuple(shuffled_graph.edges()), index\n", 447 | "\n", 448 | "\n", 449 | "real_samples = [real_transcript() for _ in range(n_transcripts_slider.value)]\n", 450 | "fake_samples = [fake_transcript() for _ in range(n_transcripts_slider.value)]\n", 451 | "\n", 452 | "null_hypothesis = stats.chi_square_equal(real_samples, fake_samples)\n", 453 | "print()\n", 454 | "\n", 455 | "if null_hypothesis:\n", 456 | " print(\"Real and fake transcripts are the same distribution.\")\n", 457 | " print(\"Victor learns nothing 👌\")\n", 458 | "else:\n", 459 | " print(\"Real and fake transcripts are different distributions.\")\n", 460 | " print(\"Victor might learn something 😧\")\n", 461 | "\n", 462 | "stats.plot_comparison(real_samples, fake_samples, \"real\", \"fake\")" 463 | ] 464 | }, 465 | { 466 | "cell_type": "code", 467 | "execution_count": null, 468 | "id": "304e2bb6", 469 | "metadata": {}, 470 | "outputs": [], 471 | "source": [] 472 | } 473 | ], 474 | "metadata": { 475 | "kernelspec": { 476 | "display_name": "Python 3 (ipykernel)", 477 | "language": "python", 478 | "name": "python3" 479 | }, 480 | "language_info": { 481 | "codemirror_mode": { 482 | "name": "ipython", 483 | "version": 3 484 | }, 485 | "file_extension": ".py", 486 | "mimetype": "text/x-python", 487 | "name": "python", 488 | "nbconvert_exporter": "python", 489 | "pygments_lexer": "ipython3", 490 | "version": "3.10.12" 491 | } 492 | }, 493 | "nbformat": 4, 494 | "nbformat_minor": 5 495 | } 496 | -------------------------------------------------------------------------------- /easy/schnorr.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "hindu-prevention", 6 | "metadata": {}, 7 | "source": [ 8 | "# Schnorr\n", 9 | "\n", 10 | "In this chapter we prove knowledge of a scalar using the Schnorr identification protocol." 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "e882e59e", 16 | "metadata": {}, 17 | "source": [ 18 | "# What is Schnorr?\n", 19 | "\n", 20 | "The [Schnorr identification protocol](https://en.wikipedia.org/wiki/Schnorr_signature) allows Peggy to prove her identity to Victor. Her identity is a curve point (public key). She proves her identity using the discrete logarithm of this point (secret key) which is a scalar." 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "id": "8aa6675d", 26 | "metadata": {}, 27 | "source": [ 28 | "# What are we proving?\n", 29 | "\n", 30 | "Peggy and Victor are engaged in an interactive proof.\n", 31 | "\n", 32 | "There is a curve point (public key).\n", 33 | "\n", 34 | "Peggy (thinks she) knows the discrete logarithm of this point (secret key). She wants to convince Victor of this fact without revealing the logarithm.\n", 35 | "\n", 36 | "Victor is sceptical and wants to see evidence. He wants to expose Peggy as a liar if her logarithm doesn't match the curve point.\n", 37 | "\n", 38 | "Peggy wins if she convinces Victor. Victor wins by accepting only matching logarithms." 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "id": "16e4623d", 44 | "metadata": {}, 45 | "source": [ 46 | "# Set up Jupyter\n", 47 | "\n", 48 | "Run the following snippet to set up your Jupyter notebook for the workshop." 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "id": "05afaf9e", 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "import sys\n", 59 | "\n", 60 | "# Add project root so we can import local modules\n", 61 | "root_dir = sys.path.append(\"..\")\n", 62 | "sys.path.append(root_dir)\n", 63 | "\n", 64 | "# Import here so cells don't depend on each other\n", 65 | "from IPython.display import display\n", 66 | "from typing import List, Tuple, Dict\n", 67 | "import ipywidgets as widgets\n", 68 | "import random\n", 69 | "\n", 70 | "from local.ec.static import Scalar, CurvePoint, ONE_POINT, NUMBER_POINTS\n", 71 | "import local.stats as stats" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "id": "64facb14", 77 | "metadata": {}, 78 | "source": [ 79 | "# Select the scenario\n", 80 | "\n", 81 | "Choose the good or the evil scenario. See how it affects the other cells further down.\n", 82 | "\n", 83 | "1. **Peggy is honest** 😇 She knows the logarithm. She wants to convince Victor of a true statement.\n", 84 | "2. **Peggy is lying** 😈 She doesn't actually know anything! She tries to fool Victor into believing a false statement." 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": null, 90 | "id": "a4518e2b", 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "def generate_keys(values: Dict[str, bool]):\n", 95 | " global P, x\n", 96 | " \n", 97 | " x = Scalar.random()\n", 98 | " if honest_dropdown.value:\n", 99 | " # Good: x is the logarithm of P\n", 100 | " P = ONE_POINT * x\n", 101 | " else:\n", 102 | " # Evil: x is (likely) not the logarithm of P\n", 103 | " P = ONE_POINT * Scalar.random()\n", 104 | "\n", 105 | "honest_dropdown = widgets.Dropdown(\n", 106 | " options=[\n", 107 | " (\"Peggy knows the logarithm 😇\", True),\n", 108 | " (\"Peggy knows nothing 😈\", False)],\n", 109 | " value=True,\n", 110 | " description=\"Scenario:\",\n", 111 | ")\n", 112 | "honest_dropdown.observe(generate_keys, names=\"value\")\n", 113 | "\n", 114 | "# Generate default keys\n", 115 | "generate_keys({})\n", 116 | "# Display dropdown\n", 117 | "display(honest_dropdown)" 118 | ] 119 | }, 120 | { 121 | "cell_type": "markdown", 122 | "id": "03b576dc", 123 | "metadata": {}, 124 | "source": [ 125 | "# How the proof roughly goes\n", 126 | "\n", 127 | "Victor knows a point $P$. Peggy knows a scalar. She wants to prove that her scalar is the discrete logarithm of the point.\n", 128 | "\n", 129 | "1. Peggy sends a random point to Victor.\n", 130 | "1. Victor sends a random scalar to Peggy.\n", 131 | "1. Peggy computes a scalar from the random values which were exchanged and from the discrete logarithm of $P$. Peggy sends the scalar to Victor.\n", 132 | "1. Victor verifies that Peggy computed the scalar correctly.\n", 133 | "\n", 134 | "# Why the proof works\n", 135 | "\n", 136 | "The scalar that Peggy has to compute requires knowledge of the discrete logarithm of $P$. It is exponentially unlikely that Peggy computes a scalar that passes Victor's check without this knowledge. If Peggy's scalar passes Victor's check, he is confident that Peggy must know the discrete logarithm ✅\n", 137 | "\n", 138 | "The random point that Peggy sends (together with its discrete logarithm) serve as blinding factors. This makes sure that Victor sees random noise ✅" 139 | ] 140 | }, 141 | { 142 | "cell_type": "markdown", 143 | "id": "200cd37f", 144 | "metadata": {}, 145 | "source": [ 146 | "# How the proof precisely goes\n", 147 | "\n", 148 | "[A useful flow chat can be found online.](https://www.zkdocs.com/docs/zkdocs/zero-knowledge-protocols/schnorr/)\n", 149 | "\n", 150 | "Victor knows the point $P$. Peggy knows a scalar $x$. She wants to prove that $P = I * x$ where $I$ is the one-point.\n", 151 | "\n", 152 | "1. Peggy generates a random scalar $r$ (nonce).\n", 153 | "1. Peggy sends $R = I *r $ (nonce point) to Victor.\n", 154 | "1. Victor sends a random scalar $e$ (challenge) to Peggy.\n", 155 | "1. Peggy sends the scalar $s = r + e * x$ (response) to Victor.\n", 156 | "1. Victor verifies that the equation $I * s \\overset{?}{=} R + P * e$ holds." 157 | ] 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": null, 162 | "id": "solar-satisfaction", 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "class Peggy:\n", 167 | " def __init__(self, x: Scalar):\n", 168 | " \"\"\"\n", 169 | " 0. Give Peggy her scalar x.\n", 170 | " \"\"\"\n", 171 | " self.x = x\n", 172 | " \n", 173 | " def commit(self) -> CurvePoint:\n", 174 | " \"\"\"\n", 175 | " 1. Peggy generates a random scalar r.\n", 176 | " \n", 177 | " 2. Peggy computes the point R = I * r and sends it to Victor.\n", 178 | " \"\"\"\n", 179 | " self.r = Scalar.random()\n", 180 | " R = ONE_POINT * self.r\n", 181 | " return R\n", 182 | " \n", 183 | " def respond(self, e: Scalar) -> Scalar:\n", 184 | " \"\"\"\n", 185 | " 4. Peggy responds by sending the scalar s = r + e * x to Victor.\n", 186 | " \"\"\"\n", 187 | " s = self.r + e * self.x\n", 188 | " return s\n", 189 | "\n", 190 | "class Victor:\n", 191 | " def __init__(self, P: CurvePoint):\n", 192 | " \"\"\"\n", 193 | " 0. Give Victor his point P.\n", 194 | " \"\"\"\n", 195 | " self.P = P\n", 196 | " \n", 197 | " def challenge(self, R: CurvePoint) -> Scalar:\n", 198 | " \"\"\"\n", 199 | " 3. Victor challenges Peggy with a random scalar e.\n", 200 | " \"\"\"\n", 201 | " self.R = R\n", 202 | " self.e = Scalar.random()\n", 203 | " return self.e\n", 204 | " \n", 205 | " def verify(self, s: Scalar) -> bool:\n", 206 | " \"\"\"\n", 207 | " 5. Victor verifies that the equation I * s =? R + P * e holds.\n", 208 | " \"\"\"\n", 209 | " return ONE_POINT * s == self.R + self.P * e" 210 | ] 211 | }, 212 | { 213 | "cell_type": "markdown", 214 | "id": "16312cc2", 215 | "metadata": {}, 216 | "source": [ 217 | "# Run the proof\n", 218 | "\n", 219 | "Let's see the proof in action.\n", 220 | "\n", 221 | "Run the Python code below and see what happens.\n", 222 | "\n", 223 | "The outcome depends on the scenario you picked. The outcome is also randomly different each time.\n", 224 | "\n", 225 | "Feel free to run the code multiple times!" 226 | ] 227 | }, 228 | { 229 | "cell_type": "code", 230 | "execution_count": null, 231 | "id": "c492d1f7", 232 | "metadata": {}, 233 | "outputs": [], 234 | "source": [ 235 | "peggy = Peggy(x)\n", 236 | "victor = Victor(P)\n", 237 | "\n", 238 | "R = peggy.commit()\n", 239 | "print(f\"R = {R}\")\n", 240 | "\n", 241 | "e = victor.challenge(R)\n", 242 | "print(f\"e = {e}\")\n", 243 | "\n", 244 | "s = peggy.respond(e)\n", 245 | "print(f\"s = {s}\")\n", 246 | "print()\n", 247 | "\n", 248 | "if victor.verify(s):\n", 249 | " if honest_dropdown.value:\n", 250 | " print(\"Victor is convinced 👌 (expected)\")\n", 251 | " else:\n", 252 | " print(\"Victor is convinced 👌 (Victor was fooled)\")\n", 253 | "else:\n", 254 | " if honest_dropdown.value:\n", 255 | " print(\"Victor is not convinced... 🤨 (Peggy was dumb)\")\n", 256 | " else:\n", 257 | " print(\"Victor is not convinced... 🤨 (expected)\")" 258 | ] 259 | }, 260 | { 261 | "cell_type": "markdown", 262 | "id": "c304d001", 263 | "metadata": {}, 264 | "source": [ 265 | "# How the proof is complete\n", 266 | "\n", 267 | "If Peggy knows the discrete logarithm, then **Victor will always be convinced** by her proof.\n", 268 | "\n", 269 | "This is because Peggy is always able to compute a scalar that passes Victor's check.\n", 270 | "\n", 271 | "Let's run a couple of exchanges and see how they go." 272 | ] 273 | }, 274 | { 275 | "cell_type": "code", 276 | "execution_count": null, 277 | "id": "8752fbec", 278 | "metadata": {}, 279 | "outputs": [], 280 | "source": [ 281 | "n_exchanges_complete_slider = widgets.IntSlider(min=10, max=1000, value=10, step=10, description=\"#Exchanges\")\n", 282 | "n_exchanges_complete_slider" 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": null, 288 | "id": "electric-samba", 289 | "metadata": {}, 290 | "outputs": [], 291 | "source": [ 292 | "# Good scenario:\n", 293 | "# Peggy knows the discrete logarithm\n", 294 | "x2 = Scalar.random()\n", 295 | "P2 = ONE_POINT * x2\n", 296 | "\n", 297 | "honest_peggy = Peggy(x2)\n", 298 | "victor = Victor(P2)\n", 299 | "\n", 300 | "peggy_success = 0\n", 301 | "\n", 302 | "for _ in range(n_exchanges_complete_slider.value):\n", 303 | " R = honest_peggy.commit()\n", 304 | " e = victor.challenge(R)\n", 305 | " s = honest_peggy.respond(e)\n", 306 | "\n", 307 | " if victor.verify(s):\n", 308 | " peggy_success += 1\n", 309 | " \n", 310 | "peggy_success_rate = peggy_success / n_exchanges_complete_slider.value * 100\n", 311 | "\n", 312 | "print(f\"Running {n_exchanges_complete_slider.value} exchanges.\")\n", 313 | "print(f\"Honest Peggy wins {peggy_success_rate:0.2f}% of the time.\")\n", 314 | "print()\n", 315 | "\n", 316 | "assert peggy_success_rate == 100\n", 317 | "print(\"Peggy always wins if she is honest.\")" 318 | ] 319 | }, 320 | { 321 | "cell_type": "markdown", 322 | "id": "42888c49", 323 | "metadata": {}, 324 | "source": [ 325 | "# How the proof is sound\n", 326 | "\n", 327 | "If Peggy doesn't know the discrete logarithm, then **Victor will almost always reject** her proof.\n", 328 | "\n", 329 | "It is exponentially unlikely that she finds a scalar $s$ that satisfies Victor's equation. Finding $s$ amounts to finding the discrete logarithm of $P$ because the discrete logarithm can be computed directly from $s$ and the blinding factors.\n", 330 | "\n", 331 | "The beautiful thing about Schnorr is that the proof is secure after a **single round**. This makes the proof very short.\n", 332 | "\n", 333 | "In most interactive proofs, Peggy and Victor must repeat their exchange a couple of rounds to increase security.\n", 334 | "\n", 335 | "Let's run a couple of exchanges and see how they go." 336 | ] 337 | }, 338 | { 339 | "cell_type": "code", 340 | "execution_count": null, 341 | "id": "0b7bca14", 342 | "metadata": {}, 343 | "outputs": [], 344 | "source": [ 345 | "n_exchanges_sound_slider = widgets.IntSlider(min=10, max=1000, value=10, step=10, description=\"#Exchanges\")\n", 346 | "n_exchanges_sound_slider" 347 | ] 348 | }, 349 | { 350 | "cell_type": "code", 351 | "execution_count": null, 352 | "id": "1bbfe52e", 353 | "metadata": {}, 354 | "outputs": [], 355 | "source": [ 356 | "# Evil scenario:\n", 357 | "# Peggy doesn't know the discrete logarithm\n", 358 | "x3 = Scalar.random()\n", 359 | "P3 = ONE_POINT * Scalar.random()\n", 360 | "\n", 361 | "lying_peggy = Peggy(x3)\n", 362 | "victor = Victor(P3)\n", 363 | "\n", 364 | "victor_success = 0\n", 365 | "\n", 366 | "for _ in range(n_exchanges_sound_slider.value):\n", 367 | " R = lying_peggy.commit()\n", 368 | " e = victor.challenge(R)\n", 369 | " s = lying_peggy.respond(e)\n", 370 | "\n", 371 | " if not victor.verify(s):\n", 372 | " victor_success += 1\n", 373 | "\n", 374 | "victor_success_rate = victor_success / n_exchanges_sound_slider.value * 100\n", 375 | "\n", 376 | "print(f\"Running {n_exchanges_sound_slider.value} exchanges.\")\n", 377 | "print(f\"Victor wins against lying Peggy {victor_success_rate:0.2f}% of the time.\")\n", 378 | "print()\n", 379 | "\n", 380 | "if victor_success_rate < 90:\n", 381 | " print(\"Victor may be fooled if Peggy randomly guesses the correct logarithm.\")\n", 382 | "else:\n", 383 | " print(\"It is basically impossible to fool Victor.\")\n", 384 | "\n", 385 | "print()\n", 386 | "print(\"The chance that Peggy guesses the logarithm of P decreases with the size of the curve.\")\n", 387 | "print(f\"We use a tiny curve with {NUMBER_POINTS} points.\")\n", 388 | "print(\"secp256k1 has ~2²⁵⁶ points!\")" 389 | ] 390 | }, 391 | { 392 | "cell_type": "markdown", 393 | "id": "10abf3a7", 394 | "metadata": {}, 395 | "source": [ 396 | "# How the proof is zero-knowledge\n", 397 | "\n", 398 | "The proof itself looks like random noise. Nothing can be extracted from this noise.\n", 399 | "\n", 400 | "Everything that is sent over the wire is randomized:\n", 401 | "\n", 402 | "1. Peggy sends a random point.\n", 403 | "1. Victor sends a random scalar.\n", 404 | "1. Peggy sends a scalar which depends on the first two values. This scalar includes a random blinding factor.\n", 405 | "\n", 406 | "The transcripts follow a pattern: Two values are random and one value is completely determined by the first two. Two variables and one equation.\n", 407 | "\n", 408 | "We can replicate this pattern:\n", 409 | "\n", 410 | "1. Compute a random scalar $e$\n", 411 | "1. Compute a random scalar $s$\n", 412 | "1. Compute the point $R$ from the other values to satisfy the Victor's equation $I * s = R + P * e$.\n", 413 | "\n", 414 | "We transform Victor's equation to obtain $R = I * s - P * s$. This is what we compute.\n", 415 | "\n", 416 | "Let's run a chi-square test to see if the original transcripts are distinguishable from the fake transcripts." 417 | ] 418 | }, 419 | { 420 | "cell_type": "code", 421 | "execution_count": null, 422 | "id": "78333fab", 423 | "metadata": {}, 424 | "outputs": [], 425 | "source": [ 426 | "n_transcripts_slider = widgets.IntSlider(min=1000, max=50000, value=10000, step=1000, description=\"#Transcripts\")\n", 427 | "n_transcripts_slider" 428 | ] 429 | }, 430 | { 431 | "cell_type": "code", 432 | "execution_count": null, 433 | "id": "alert-exhibit", 434 | "metadata": { 435 | "scrolled": false 436 | }, 437 | "outputs": [], 438 | "source": [ 439 | "peggy = Peggy(x)\n", 440 | "victor = Victor(P)\n", 441 | "\n", 442 | "def real_transcript() -> Tuple:\n", 443 | " R = peggy.commit()\n", 444 | " e = victor.challenge(R)\n", 445 | " s = peggy.respond(e)\n", 446 | " return R, e, s\n", 447 | "\n", 448 | "\n", 449 | "def fake_transcript() -> Tuple:\n", 450 | " e = Scalar.random()\n", 451 | " s = Scalar.random()\n", 452 | " R = ONE_POINT * s - P * e\n", 453 | " return R, e, s\n", 454 | "\n", 455 | "\n", 456 | "print(\"Real transcript: {}\".format(real_transcript()))\n", 457 | "print(\"Fake transcript: {}\".format(fake_transcript()))\n", 458 | "print()\n", 459 | "\n", 460 | "real_samples = [real_transcript() for _ in range(n_transcripts_slider.value)]\n", 461 | "fake_samples = [fake_transcript() for _ in range(n_transcripts_slider.value)]\n", 462 | "\n", 463 | "null_hypothesis = stats.chi_square_equal(real_samples, fake_samples)\n", 464 | "print()\n", 465 | "\n", 466 | "if null_hypothesis:\n", 467 | " print(\"Real and fake transcripts are the same distribution.\")\n", 468 | " print(\"Victor learns nothing 👌\")\n", 469 | "else:\n", 470 | " print(\"Real and fake transcripts are different distributions.\")\n", 471 | " print(\"Victor might learn something 😧\")\n", 472 | "\n", 473 | "stats.plot_comparison(real_samples, fake_samples, \"real\", \"fake\")" 474 | ] 475 | }, 476 | { 477 | "cell_type": "code", 478 | "execution_count": null, 479 | "id": "b339f10b", 480 | "metadata": {}, 481 | "outputs": [], 482 | "source": [] 483 | } 484 | ], 485 | "metadata": { 486 | "kernelspec": { 487 | "display_name": "Python 3 (ipykernel)", 488 | "language": "python", 489 | "name": "python3" 490 | }, 491 | "language_info": { 492 | "codemirror_mode": { 493 | "name": "ipython", 494 | "version": 3 495 | }, 496 | "file_extension": ".py", 497 | "mimetype": "text/x-python", 498 | "name": "python", 499 | "nbconvert_exporter": "python", 500 | "pygments_lexer": "ipython3", 501 | "version": "3.10.12" 502 | } 503 | }, 504 | "nbformat": 4, 505 | "nbformat_minor": 5 506 | } 507 | -------------------------------------------------------------------------------- /graph/isomorphism.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "d4bde784", 6 | "metadata": {}, 7 | "source": [ 8 | "# Graph Isomorphism\n", 9 | "\n", 10 | "In this chapter we construct a zero-knowledge protocol around graph isomorphism.\n", 11 | "\n", 12 | "This chapter is based on [a lecture from the Max Plank Institute for Informatics](https://resources.mpi-inf.mpg.de/departments/d1/teaching/ss13/gitcs/lecture9.pdf)." 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "3c8954df", 18 | "metadata": {}, 19 | "source": [ 20 | "# What is a graph?\n", 21 | "\n", 22 | "[A graph](https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)) consists of nodes and edges. Nodes are points in space. Edges are bridges between nodes.\n", 23 | "\n", 24 | "# What is an isomorphism?\n", 25 | "\n", 26 | "[Two graphs are isomorphic](https://en.wikipedia.org/wiki/Graph_isomorphism) if they have the same structure. By changing the names of the nodes of the first graph, we can obtain the second graph, and vice versa. There exists a translation of node names.\n", 27 | "\n", 28 | "Given two large random graphs, it is hard to know if they are isomorphic. There is no known algorithm to efficiently compute this (in polynomial time)." 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "id": "b27ff670", 34 | "metadata": {}, 35 | "source": [ 36 | "# What are we proving?\n", 37 | "\n", 38 | "Peggy and Victor are engaged in an interactive proof.\n", 39 | "\n", 40 | "There are two graphs.\n", 41 | "\n", 42 | "Peggy thinks she knows a translation between both graphs (both graphs are isomorphic). She wants to prove that to Victor, without revealing the translation.\n", 43 | "\n", 44 | "Victor is sceptical and wants to see evidence. He wants to expose Peggy as a liar if both graphs are structually different (non-isomorphic).\n", 45 | "\n", 46 | "Peggy wins if she convinces Victor. Victor wins by accepting only graphs that are isomorphic." 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "id": "f38168f6", 52 | "metadata": {}, 53 | "source": [ 54 | "# Set up Jupyter\n", 55 | "\n", 56 | "Run the following snippet to set up your Jupyter notebook for the workshop." 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": null, 62 | "id": "df0042c6", 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "import os\n", 67 | "import sys\n", 68 | "\n", 69 | "# Add project root so we can import local modules\n", 70 | "root_dir = sys.path.append(\"..\")\n", 71 | "sys.path.append(root_dir)\n", 72 | "\n", 73 | "# Import here so cells don't depend on each other\n", 74 | "from IPython.display import display\n", 75 | "from typing import List, Tuple, Dict\n", 76 | "import ipywidgets as widgets\n", 77 | "import random\n", 78 | "import networkx as nx\n", 79 | "import matplotlib.pyplot as plt\n", 80 | "\n", 81 | "from local.graph import Mapping, random_graph, non_isomorphic_graph\n", 82 | "import local.stats as stats" 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "id": "4eb0d72d", 88 | "metadata": {}, 89 | "source": [ 90 | "# Select the scenario\n", 91 | "\n", 92 | "Choose the good or the evil scenario. See how it affects the other cells further down.\n", 93 | "\n", 94 | "1. **Peggy is honest** 😇 She knows a translation between both graphs. She wants to convince Victor of a true statement.\n", 95 | "2. **Peggy is lying** 😈 She doesn't know a translation between both graphs! She tries to fool Victor into believing a false statement.\n", 96 | "\n", 97 | "Also select the **size of the graphs**." 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "id": "643ba49f", 104 | "metadata": {}, 105 | "outputs": [], 106 | "source": [ 107 | "def generate_graphs(values: Dict):\n", 108 | " global graph1, graph2, from_1_to_2\n", 109 | " \n", 110 | " n_edges = n_nodes_slider.value\n", 111 | " graph1 = random_graph(n_nodes_slider.value, n_edges)\n", 112 | "\n", 113 | " if honest_dropdown.value:\n", 114 | " # Good: There is a translation between both graphs\n", 115 | " from_1_to_2 = Mapping.shuffle_graph(graph1)\n", 116 | " graph2 = from_1_to_2.apply_graph(graph1)\n", 117 | " else:\n", 118 | " # Evil: Both graphs are non-isomorphic\n", 119 | " graph2 = non_isomorphic_graph(graph1)\n", 120 | "\n", 121 | "honest_dropdown = widgets.Dropdown(\n", 122 | " options=[\n", 123 | " (\"Peggy can translate 😇\", True),\n", 124 | " (\"Peggy cannot translate 😈\", False)],\n", 125 | " value=True,\n", 126 | " description=\"Scenario:\",\n", 127 | ")\n", 128 | "honest_dropdown.observe(generate_graphs, names=\"value\")\n", 129 | "\n", 130 | "n_nodes_slider = widgets.IntSlider(min=4, max=20, value=4, step=1, description=\"#Nodes\")\n", 131 | "n_nodes_slider.observe(generate_graphs, names=\"value\")\n", 132 | "\n", 133 | "# Generate default values\n", 134 | "generate_graphs({})\n", 135 | "# Display selection\n", 136 | "display(honest_dropdown)\n", 137 | "display(n_nodes_slider)" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "id": "75513058", 143 | "metadata": {}, 144 | "source": [ 145 | "# Visualize your graphs\n", 146 | "\n", 147 | "Visualize the graphs you generated." 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": null, 153 | "id": "c3787e16", 154 | "metadata": {}, 155 | "outputs": [], 156 | "source": [ 157 | "print(\"Graph 1\")\n", 158 | "nx.draw(graph1, with_labels=True)\n", 159 | "plt.show()\n", 160 | "\n", 161 | "print(\"Graph 2\")\n", 162 | "nx.draw(graph2, with_labels=True)\n", 163 | "plt.show()" 164 | ] 165 | }, 166 | { 167 | "cell_type": "markdown", 168 | "id": "d4b080be", 169 | "metadata": {}, 170 | "source": [ 171 | "# How the proof goes\n", 172 | "\n", 173 | "1. Peggy randomly shuffles graph 1 to obtain graph $S$.\n", 174 | "1. Peggy sends $S$ to Victor.\n", 175 | "1. Victor randomly chooses graph 1 or graph 2, to obtain graph $C$.\n", 176 | "1. Peggy computes a translation $t$ from $C$ graph to $S$.\n", 177 | "1. Victor checks that $t$ translates $C$ to $S$.\n", 178 | "\n", 179 | "How Peggy computes $t$ depends on Victor's choice:\n", 180 | "\n", 181 | "1. If Victor chooses graph 1, then $t$ is simply the shuffling from step 1. This translates graph 1 to $S$.\n", 182 | "1. If Victor chooses graph 2, then $t$ translates graph 2 to graph 1 and then it applies the shuffling from step 1. This translates graph 2 to graph 1 to $S$.\n", 183 | "\n", 184 | "If Peggy can translate between graph 1 and graph 2, then she can compute translations in both directions." 185 | ] 186 | }, 187 | { 188 | "cell_type": "code", 189 | "execution_count": null, 190 | "id": "23b6d019", 191 | "metadata": { 192 | "scrolled": true 193 | }, 194 | "outputs": [], 195 | "source": [ 196 | "class Peggy:\n", 197 | " def __init__(self, graph1: nx.Graph, from_1_to_2: Mapping):\n", 198 | " self._graph1 = graph1\n", 199 | " self._from_1_to_2 = from_1_to_2\n", 200 | " \n", 201 | " def shuffled_graph(self) -> nx.Graph:\n", 202 | " self._shuffle = Mapping.shuffle_graph(self._graph1)\n", 203 | " shuffled_graph1 = self._shuffle.apply_graph(self._graph1)\n", 204 | " return shuffled_graph1\n", 205 | " \n", 206 | " def respond(self, index: int) -> Mapping:\n", 207 | " if index == 0:\n", 208 | " return self._shuffle\n", 209 | " else:\n", 210 | " assert index == 1\n", 211 | " complex_shuffle = self._from_1_to_2.invert().and_then(self._shuffle)\n", 212 | " return complex_shuffle\n", 213 | "\n", 214 | "\n", 215 | "class Victor:\n", 216 | " def __init__(self, graph1: nx.Graph, graph2: nx.Graph):\n", 217 | " self._graphs = [graph1, graph2]\n", 218 | " \n", 219 | " def random_index(self, shuffled_graph: nx.Graph) -> int:\n", 220 | " self._shuffled_graph = shuffled_graph\n", 221 | " self._index = random.randrange(0, 2)\n", 222 | " return self._index\n", 223 | " \n", 224 | " def verify(self, shuffle: Mapping) -> bool:\n", 225 | " another_shuffled_graph = shuffle.apply_graph(self._graphs[self._index])\n", 226 | " # `self._shuffled_graph == another_shuffled_graph` compares pointers not data\n", 227 | " return set(sorted(self._shuffled_graph.edges())) == set(sorted(another_shuffled_graph.edges()))" 228 | ] 229 | }, 230 | { 231 | "cell_type": "markdown", 232 | "id": "48422c50", 233 | "metadata": {}, 234 | "source": [ 235 | "# Run the proof\n", 236 | "\n", 237 | "Let's see the proof in action.\n", 238 | "\n", 239 | "Run the Python code below and see what happens.\n", 240 | "\n", 241 | "The outcome depends on the scenario you picked. The outcome is also randomly different each time.\n", 242 | "\n", 243 | "Feel free to run the code multiple times!" 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": null, 249 | "id": "7c1bca4f", 250 | "metadata": {}, 251 | "outputs": [], 252 | "source": [ 253 | "peggy = Peggy(graph1, from_1_to_2)\n", 254 | "victor = Victor(graph1, graph2)\n", 255 | "\n", 256 | "shuffled_graph = peggy.shuffled_graph()\n", 257 | "index = victor.random_index(shuffled_graph)\n", 258 | "response_shuffle = peggy.respond(index)\n", 259 | "\n", 260 | "if victor.verify(response_shuffle):\n", 261 | " if honest_dropdown.value:\n", 262 | " print(\"Victor is convinced 👌 (expected)\")\n", 263 | " else:\n", 264 | " print(\"Victor is convinced 👌 (Victor was fooled)\")\n", 265 | "else:\n", 266 | " if honest_dropdown.value:\n", 267 | " print(\"Victor is not convinced... 🤨 (Peggy was dumb)\")\n", 268 | " else:\n", 269 | " print(\"Victor is not convinced... 🤨 (expected)\")" 270 | ] 271 | }, 272 | { 273 | "cell_type": "markdown", 274 | "id": "9ea92bd8", 275 | "metadata": {}, 276 | "source": [ 277 | "# How the proof is complete\n", 278 | "\n", 279 | "If Peggy can translate between both graphs, then **Victor will always be convinced** by her proof.\n", 280 | "\n", 281 | "This is because Peggy is always able to produce a translation from $C$ to $S$.\n", 282 | "\n", 283 | "Let's run a couple of exchanges and see how they go." 284 | ] 285 | }, 286 | { 287 | "cell_type": "code", 288 | "execution_count": null, 289 | "id": "d9491ddf", 290 | "metadata": {}, 291 | "outputs": [], 292 | "source": [ 293 | "n_exchanges_complete_slider = widgets.IntSlider(min=10, max=1000, value=10, step=10, description=\"#Exchanges\")\n", 294 | "n_exchanges_complete_slider" 295 | ] 296 | }, 297 | { 298 | "cell_type": "code", 299 | "execution_count": null, 300 | "id": "60792496", 301 | "metadata": {}, 302 | "outputs": [], 303 | "source": [ 304 | "# Good scenario:\n", 305 | "# There is a translation between both graphs\n", 306 | "from_1_to_3 = Mapping.shuffle_graph(graph1)\n", 307 | "graph3 = from_1_to_3.apply_graph(graph1)\n", 308 | "\n", 309 | "honest_peggy = Peggy(graph1, from_1_to_3)\n", 310 | "victor = Victor(graph1, graph3)\n", 311 | "\n", 312 | "peggy_success = 0\n", 313 | "\n", 314 | "for _ in range(n_exchanges_complete_slider.value):\n", 315 | " shuffled_graph = honest_peggy.shuffled_graph()\n", 316 | " index = victor.random_index(shuffled_graph)\n", 317 | " response_shuffle = honest_peggy.respond(index)\n", 318 | "\n", 319 | " if victor.verify(response_shuffle):\n", 320 | " peggy_success += 1\n", 321 | " \n", 322 | "peggy_success_rate = peggy_success / n_exchanges_complete_slider.value * 100\n", 323 | "\n", 324 | "print(f\"Running {n_exchanges_complete_slider.value} exchanges.\")\n", 325 | "print(f\"Honest Peggy wins {peggy_success_rate:0.2f}% of the time.\")\n", 326 | "print()\n", 327 | "\n", 328 | "assert peggy_success_rate == 100\n", 329 | "print(\"Peggy always wins if she is honest.\")" 330 | ] 331 | }, 332 | { 333 | "cell_type": "markdown", 334 | "id": "9c3c78bc", 335 | "metadata": {}, 336 | "source": [ 337 | "# How the proof is sound\n", 338 | "\n", 339 | "If Peggy cannot translate between both graphs, then **Victor has a chance to reject** her proof.\n", 340 | "\n", 341 | "Assuming that Victor randomly chooses between both graphs, Peggy has a 50% chance to produce a correct translation. This is the case when Victor chooses graph 1. Peggy learned how to translate graph 1 to $S$ in step 1!\n", 342 | "\n", 343 | "Peggy fails if Victor chooses graph 2, because translating from graph 2 to graph 1 requires Peggy to know the translation in the first place. This case occurs 50% of the time. The probabilities don't look great for Victor.\n", 344 | "\n", 345 | "We can increase Victor's confidence by running the protocol for **multiple rounds**. This means Peggy randomly shuffles and Victor randomly selects a graph multiple times. Each time, Peggy has to produce a translation from $C$ to $S$. Victor accepts if Peggy answered correctly **all** time times. However, he rejects if Peggy answers incorrectly **even once**.\n", 346 | "\n", 347 | "The chance that Peggy answers correctly for $n$ rounds, without being able to translate between both graphs, is $\\left(\\frac{1}{2}\\right)^n$. This decreases exponentially in $n$ and becomes tiny! If Peggy answers correctly, then Victor is confident that she didn't cheat.\n", 348 | "\n", 349 | "Let's run a couple of exchanges and see how they go." 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": null, 355 | "id": "c74881a2", 356 | "metadata": {}, 357 | "outputs": [], 358 | "source": [ 359 | "n_exchanges_sound_slider = widgets.IntSlider(min=10, max=1000, value=10, step=10, description=\"#Exchanges\")\n", 360 | "n_rounds_slider = widgets.IntSlider(min=1, max=10, value=1, step=1, description=\"#Rounds\")\n", 361 | "\n", 362 | "display(n_exchanges_sound_slider)\n", 363 | "display(n_rounds_slider)" 364 | ] 365 | }, 366 | { 367 | "cell_type": "code", 368 | "execution_count": null, 369 | "id": "fa919e3b", 370 | "metadata": {}, 371 | "outputs": [], 372 | "source": [ 373 | "# Evil scenario:\n", 374 | "# Both graphs are non-isomorphic\n", 375 | "graph4 = non_isomorphic_graph(graph1)\n", 376 | "from_1_to_4 = Mapping.shuffle_graph(graph1)\n", 377 | "\n", 378 | "lying_peggy = Peggy(graph1, from_1_to_4)\n", 379 | "victor = Victor(graph1, graph4)\n", 380 | "\n", 381 | "victor_success = 0\n", 382 | "\n", 383 | "for _ in range(n_exchanges_sound_slider.value):\n", 384 | " for _ in range(n_rounds_slider.value):\n", 385 | " shuffled_graph = lying_peggy.shuffled_graph()\n", 386 | " index = victor.random_index(shuffled_graph)\n", 387 | " response_shuffle = lying_peggy.respond(index)\n", 388 | " \n", 389 | " if not victor.verify(response_shuffle):\n", 390 | " victor_success += 1\n", 391 | " break\n", 392 | " \n", 393 | "victor_success_rate = victor_success / n_exchanges_sound_slider.value * 100\n", 394 | "\n", 395 | "print(f\"Running {n_exchanges_sound_slider.value} exchanges with {n_rounds_slider.value} rounds each.\")\n", 396 | "print(f\"Victor wins against lying Peggy {victor_success_rate:0.2f}% of the time.\")\n", 397 | "print()\n", 398 | "\n", 399 | "if victor_success_rate < 50:\n", 400 | " print(\"Victor loses quite often for a small number of rounds.\")\n", 401 | "elif victor_success_rate < 90:\n", 402 | " print(\"Victor gains more confidence with each added round.\")\n", 403 | "else:\n", 404 | " print(\"At some point it is basically impossible to fool Victor.\")" 405 | ] 406 | }, 407 | { 408 | "cell_type": "markdown", 409 | "id": "7859e817", 410 | "metadata": {}, 411 | "source": [ 412 | "# How the proof is zero-knowledge\n", 413 | "\n", 414 | "The proof itself looks like random noise. Nothing can be extracted from this noise.\n", 415 | "\n", 416 | "Everything that is sent over the wire is randomized:\n", 417 | "\n", 418 | "1. Peggy sends a randomly shuffled graph.\n", 419 | "1. Victor sends a random index.\n", 420 | "1. Peggy sends a translation which includes the random shuffling from step 1. This looks like a random mapping.\n", 421 | "\n", 422 | "We can replicate this pattern:\n", 423 | "\n", 424 | "1. Compute a random index (0 or 1).\n", 425 | "1. Randomly shuffle the graph at the index.\n", 426 | "1. Take the shuffling from step 2 as the translation.\n", 427 | "\n", 428 | "Victor verifies that $t$ translates $C$ to $S$.\n", 429 | "\n", 430 | "In the fake transcripts, $C$ is the graph at the index from step 1. $S$ is the result of step 2. By construction, $t$ from step 3 translates $C$ to $S$.\n", 431 | "\n", 432 | "Let's run a chi-square test to see if the original transcripts are distinguishable from the fake transcripts.\n", 433 | "\n", 434 | "**Try small graphs first!** They require fewer samples than large graphs." 435 | ] 436 | }, 437 | { 438 | "cell_type": "code", 439 | "execution_count": null, 440 | "id": "8a7b9bb0", 441 | "metadata": {}, 442 | "outputs": [], 443 | "source": [ 444 | "n_transcripts_slider = widgets.IntSlider(min=1000, max=50000, value=10000, step=1000, description=\"#Transcripts\")\n", 445 | "n_transcripts_slider" 446 | ] 447 | }, 448 | { 449 | "cell_type": "code", 450 | "execution_count": null, 451 | "id": "d09b788b", 452 | "metadata": { 453 | "scrolled": false 454 | }, 455 | "outputs": [], 456 | "source": [ 457 | "peggy = Peggy(graph1, from_1_to_2)\n", 458 | "victor = Victor(graph1, graph2)\n", 459 | "\n", 460 | "def real_transcript() -> Tuple:\n", 461 | " shuffled_graph = peggy.shuffled_graph()\n", 462 | " index = victor.random_index(shuffled_graph)\n", 463 | " response_shuffle = peggy.respond(index)\n", 464 | "\n", 465 | " return tuple(shuffled_graph.edges()), index, tuple(response_shuffle)\n", 466 | "\n", 467 | "\n", 468 | "def fake_transcript() -> Tuple:\n", 469 | " index = random.randrange(0, 2)\n", 470 | "\n", 471 | " if index == 0:\n", 472 | " shuffle = Mapping.shuffle_graph(graph1)\n", 473 | " shuffled_graph = shuffle.apply_graph(graph1)\n", 474 | " response_shuffle = shuffle\n", 475 | " else:\n", 476 | " shuffle = Mapping.shuffle_graph(graph2)\n", 477 | " shuffled_graph = shuffle.apply_graph(graph2)\n", 478 | " response_shuffle = shuffle\n", 479 | "\n", 480 | " return tuple(shuffled_graph.edges()), index, tuple(response_shuffle)\n", 481 | "\n", 482 | "\n", 483 | "real_samples = [real_transcript() for _ in range(n_transcripts_slider.value)]\n", 484 | "fake_samples = [fake_transcript() for _ in range(n_transcripts_slider.value)]\n", 485 | "\n", 486 | "null_hypothesis = stats.chi_square_equal(real_samples, fake_samples)\n", 487 | "print()\n", 488 | "\n", 489 | "if null_hypothesis:\n", 490 | " print(\"Real and fake transcripts are the same distribution.\")\n", 491 | " print(\"Victor learns nothing 👌\")\n", 492 | "else:\n", 493 | " print(\"Real and fake transcripts are different distributions.\")\n", 494 | " print(\"Victor might learn something 😧\")\n", 495 | "\n", 496 | "stats.plot_comparison(real_samples, fake_samples, \"real\", \"fake\")" 497 | ] 498 | }, 499 | { 500 | "cell_type": "code", 501 | "execution_count": null, 502 | "id": "8582e191", 503 | "metadata": {}, 504 | "outputs": [], 505 | "source": [] 506 | } 507 | ], 508 | "metadata": { 509 | "kernelspec": { 510 | "display_name": "Python 3 (ipykernel)", 511 | "language": "python", 512 | "name": "python3" 513 | }, 514 | "language_info": { 515 | "codemirror_mode": { 516 | "name": "ipython", 517 | "version": 3 518 | }, 519 | "file_extension": ".py", 520 | "mimetype": "text/x-python", 521 | "name": "python", 522 | "nbconvert_exporter": "python", 523 | "pygments_lexer": "ipython3", 524 | "version": "3.10.12" 525 | } 526 | }, 527 | "nbformat": 4, 528 | "nbformat_minor": 5 529 | } 530 | -------------------------------------------------------------------------------- /local/ec/core.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import Optional, Tuple, Union, List 3 | import hashlib 4 | import unittest 5 | 6 | MAX_COORDINATE = 7 7 | MINUS_ONE_COORDINATE = MAX_COORDINATE - 1 8 | 9 | 10 | class ModInt: 11 | """ 12 | Integer n modulo p, where p is prime 13 | """ 14 | value: int 15 | modulus = None 16 | """ 17 | Modulus p, should be prime 18 | """ 19 | 20 | def __init__(self, value): 21 | self.value = value % self.modulus 22 | 23 | def __add__(self, other: "ModInt") -> "ModInt": 24 | return self.__class__(self.value + other.value) 25 | 26 | def __sub__(self, other: "ModInt") -> "ModInt": 27 | return self.__class__(self.value - other.value) 28 | 29 | def __neg__(self) -> "ModInt": 30 | return self.__class__(-self.value) 31 | 32 | def __mul__(self, other: "ModInt") -> "ModInt": 33 | return self.__class__(self.value * other.value) 34 | 35 | def reciprocal(self) -> "ModInt": 36 | """ 37 | Return multiplicative inverse of self. 38 | 39 | Finds coordinate i such that self * i = 1. 40 | """ 41 | return self.__class__(pow(self.value, -1, self.modulus)) 42 | 43 | def __truediv__(self, other: "ModInt") -> "ModInt": 44 | return self * other.reciprocal() 45 | 46 | def __pow__(self, power: Union[int, "ModInt"], modulo=None) -> "ModInt": 47 | if isinstance(power, ModInt): 48 | power = power.value 49 | return self.__class__(pow(self.value, power, modulo)) 50 | 51 | def __mod__(self, modulus: Union[int, "ModInt"]): 52 | if isinstance(modulus, ModInt): 53 | modulus = modulus.value 54 | return self.__class__(self.value % modulus) 55 | 56 | def __eq__(self, other: "ModInt") -> bool: 57 | return self.value == other.value 58 | 59 | def __repr__(self) -> str: 60 | return repr(self.value) 61 | 62 | def __int__(self) -> int: 63 | return self.value 64 | 65 | def __hash__(self) -> hash: 66 | return hash(self.value) 67 | 68 | @classmethod 69 | def nth(cls, n: int) -> "ModInt": 70 | """ 71 | Return the nth modular integer. 72 | 73 | The integer n is internally scaled to the right size. 74 | """ 75 | return cls(n) 76 | 77 | @classmethod 78 | def random(cls) -> "ModInt": 79 | """ 80 | Return a uniformly random modular integer. 81 | """ 82 | return cls(random.randrange(cls.modulus)) 83 | 84 | def legendre_symbol(self) -> int: 85 | """ 86 | Return the Legendre symbol. Also known as Euler's criterion. 87 | 88 | Returns 1 if self is a quadratic residue modulo p (and self != 0). 89 | Return -1 (modulo p) if self is a quadratic nonresidue modulo p. 90 | Returns 0 if self is = 0. 91 | 92 | https://en.wikipedia.org/wiki/Legendre_symbol 93 | https://en.wikipedia.org/wiki/Euler%27s_criterion 94 | """ 95 | return (self ** ((self.modulus - 1) // 2)).value 96 | 97 | def sqrt(self) -> Optional["ModInt"]: 98 | """ 99 | Return the square root of self. 100 | 101 | Finds coordinate r such that r^2 = self. 102 | About half of all coordinates have square roots. 103 | If r is a solution, then -r is another solution. 104 | 105 | Uses the Tonelli–Shanks algorithm. 106 | 107 | https://en.wikipedia.org/wiki/Tonelli%E2%80%93Shanks_algorithm 108 | """ 109 | # Easy cases 110 | if self.value == 0: 111 | return self.__class__(0) 112 | if self.modulus == 2: 113 | return self 114 | if self.legendre_symbol() != 1: 115 | return None 116 | # This is the case for secp256k1 117 | if self.modulus % 4 == 3: 118 | return self ** ((self.modulus + 1) // 4) 119 | 120 | # Factor out powers of 2 to find q and s 121 | # such that p - 1 = q * 2^s with q odd 122 | q, s = self.modulus - 1, 0 123 | while q % 2 == 0: 124 | s += 1 125 | q //= 2 126 | assert q > 0 127 | assert q % 2 == 1 128 | 129 | # Search for z that is quadratic nonresidue 130 | # Half of all coordinates are quadratic nonresidues 131 | z = self.__class__(1) 132 | while z.legendre_symbol() + 1 != self.modulus: 133 | z.value += 1 134 | 135 | m = s 136 | c = z ** q 137 | t = self ** q 138 | r = self ** ((q + 1) // 2) 139 | 140 | while t.value != 1: 141 | # Use repeated squaring to find least 0 < i < m 142 | # such that t^(2^i) = 1 143 | i, e = 0, 2 144 | for i in range(1, m): 145 | if (t ** e).value == 1: 146 | break 147 | e *= 2 148 | 149 | b = c ** (2 ** (m - i - 1)) 150 | m = i 151 | c = b ** 2 152 | t = t * b ** 2 153 | r = r * b 154 | 155 | return r 156 | 157 | 158 | class Coordinate(ModInt): 159 | """ 160 | Coordinate of curve point 161 | """ 162 | modulus = MAX_COORDINATE 163 | 164 | def lift_x(self) -> Optional["AffinePoint"]: 165 | """ 166 | Return curve point that corresponds to given x coordinate, if such point exists. 167 | """ 168 | # y^2 = x^3 + a * x + b 169 | y_squared = self ** 3 + PARAMETER_A * self + PARAMETER_B 170 | # y_squared may or may not have a square root 171 | y = y_squared.sqrt() 172 | if y is None or y ** 2 != y_squared: 173 | return None 174 | return AffinePoint(self, y) 175 | 176 | 177 | PARAMETER_A = Coordinate(0) 178 | PARAMETER_B = Coordinate(3) 179 | 180 | 181 | class TestCoordinate(unittest.TestCase): 182 | def test_modulo(self): 183 | x = random.randrange(MAX_COORDINATE) 184 | self.assertEqual(Coordinate(x) + Coordinate(MAX_COORDINATE - x), Coordinate(0)) 185 | 186 | def test_inverse(self): 187 | x = random.randrange(MAX_COORDINATE) 188 | self.assertEqual(Coordinate(x) * Coordinate(x).reciprocal(), Coordinate(1)) 189 | 190 | def test_pow(self): 191 | x = Coordinate(random.randrange(MAX_COORDINATE)) 192 | self.assertEqual(x * x * x, x ** 3) 193 | 194 | def test_legendre_symbol(self): 195 | for x in range(MAX_COORDINATE): 196 | x = Coordinate(x) 197 | ls = x.legendre_symbol() 198 | self.assertTrue(ls == MINUS_ONE_COORDINATE or ls == 0 or ls == 1) 199 | 200 | def test_sqrt(self): 201 | for x in range(MAX_COORDINATE): 202 | y_squared = Coordinate(x) 203 | y = y_squared.sqrt() 204 | 205 | if y is not None: 206 | self.assertEqual(y ** 2, y_squared) 207 | 208 | 209 | class AffinePoint: 210 | """ 211 | Curve point in affine space. 212 | """ 213 | x: Optional[Coordinate] 214 | y: Optional[Coordinate] 215 | 216 | def __init__(self, x, y): 217 | self.x = x 218 | self.y = y 219 | 220 | def __repr__(self) -> str: 221 | if self.is_zero(): 222 | return "(zero)" 223 | else: 224 | return repr(self.xy()) 225 | 226 | def __hash__(self) -> hash: 227 | return hash((self.x, self.y)) 228 | 229 | def is_zero(self) -> bool: 230 | """ 231 | Return whether self is the zero-point. 232 | """ 233 | return self.x is None 234 | 235 | def is_on_curve(self) -> bool: 236 | """ 237 | Return whether self is on the curve. 238 | 239 | The curve is given by the equation `y^2 = x^3 + a * x + b` 240 | for some parameters a and b. 241 | Points have to satisfy this equation to be on the curve. 242 | """ 243 | return self.is_zero() or self.y ** 2 == self.x ** 3 + PARAMETER_A * self.x + PARAMETER_B 244 | 245 | def xy(self) -> Optional[Tuple[Coordinate, Coordinate]]: 246 | """ 247 | Return x and y coordinates of self, if they exist. 248 | 249 | The zero-point has no coordinates. 250 | """ 251 | if self.is_zero(): 252 | return None 253 | return self.x, self.y 254 | 255 | def __eq__(self, other: "AffinePoint") -> bool: 256 | if self.is_zero() and other.is_zero(): 257 | return True 258 | if self.is_zero() or other.is_zero(): 259 | return False 260 | return self.x == other.x and self.y == other.y 261 | 262 | def double(self) -> "AffinePoint": 263 | """ 264 | Return self + self (point doubling). 265 | 266 | https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Point_doubling 267 | """ 268 | # Zero + Zero = Zero 269 | if self.is_zero(): 270 | return ZERO_POINT 271 | # self = -self: 272 | # self + (-self) = Zero 273 | if self.y.value == 0: 274 | return ZERO_POINT 275 | 276 | s = (Coordinate(3) * self.x * self.x + PARAMETER_A) / (Coordinate(2) * self.y) 277 | x = s * s - Coordinate(2) * self.x 278 | y = s * (self.x - x) - self.y 279 | 280 | return AffinePoint(x, y) 281 | 282 | def __neg__(self) -> "AffinePoint": 283 | """ 284 | Return -self (point negation). 285 | 286 | -self is a point such that self + (-self) = Zero. 287 | """ 288 | if self.is_zero(): 289 | return self 290 | return AffinePoint(self.x, -self.y) 291 | 292 | def __add__(self, other: "AffinePoint") -> "AffinePoint": 293 | """ 294 | Return self + other (point addition). 295 | 296 | https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Point_addition 297 | """ 298 | # Zero + other = other 299 | if self.is_zero(): 300 | return other 301 | # self + Zero = self 302 | if other.is_zero(): 303 | return self 304 | if self.x == other.x: 305 | # Double point since s (below) would be division by zero 306 | if self.y == other.y: 307 | return self.double() 308 | # self + (-self) = Zero 309 | else: 310 | return ZERO_POINT 311 | 312 | s: Coordinate = (other.y - self.y) / (other.x - self.x) 313 | x = s * s - self.x - other.x 314 | y = s * (self.x - x) - self.y 315 | 316 | return AffinePoint(x, y) 317 | 318 | def __sub__(self, other: "AffinePoint") -> "AffinePoint": 319 | """ 320 | Return self - other (point subtraction). 321 | 322 | self - other is the same as self + (-other). 323 | """ 324 | return self + -other 325 | 326 | def __mul__(self, scalar: "Scalar") -> Optional["AffinePoint"]: 327 | """ 328 | Return scalar * self (scalar multiplication). 329 | 330 | Uses binary expansion (aka double and add). 331 | 332 | https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Double-and-add 333 | """ 334 | if ONE_POINT is None: 335 | return None 336 | 337 | tmp = self 338 | ret = ZERO_POINT 339 | 340 | while scalar.value != 0: 341 | if scalar.value & 1 != 0: # same as scalar.value % 2 != 0 342 | ret += tmp 343 | tmp = tmp.double() 344 | scalar = Scalar(scalar.value >> 1) # same as scalar.value // 2 345 | 346 | return ret 347 | 348 | def discrete_log(self) -> Optional["Scalar"]: 349 | """ 350 | Return scalar k such that self = One * k (discrete logarithm). 351 | 352 | Uses Pollard's rho algorithm. 353 | 354 | https://en.wikipedia.org/wiki/Pollard%27s_rho_algorithm_for_logarithms 355 | """ 356 | if ONE_POINT is None: 357 | return None 358 | 359 | def step(p: AffinePoint, a: "Scalar", b: "Scalar") -> Tuple[AffinePoint, "Scalar", "Scalar"]: 360 | if p.is_zero() or p.x.value % 3 == 0: 361 | return p + ONE_POINT, a + Scalar(1), b 362 | elif p.x.value % 3 == 1: 363 | return p + self, a, b + Scalar(1) 364 | else: 365 | return p + p, a + a, b + b 366 | 367 | # Different initializations for Tortoise 368 | # 369 | # This avoids the unlikely case b1 == b2 below 370 | # where we cannot compute the inverse of zero 371 | # 372 | # Usually this loop will run for one iteration 373 | for i in range(NUMBER_POINTS): 374 | # Loop invariant: p_i = ONE_POINT * a_i * self * b_i 375 | # Tortoise (gets a head start of i steps) 376 | p1, a1, b1 = ONE_POINT * Scalar(i), Scalar(i), Scalar(0) 377 | # assert p1 == ONE_POINT * a1 + self * b1 378 | 379 | # Hare (starts at step 0) 380 | p2, a2, b2 = ONE_POINT, Scalar(1), Scalar(0) 381 | # assert p2 == ONE_POINT * a2 + self * b2 382 | 383 | # Guaranteed to halt because group is cyclic and finite 384 | while True: 385 | # Tortoise makes one step 386 | p1, a1, b1 = step(p1, a1, b1) 387 | # assert p1 == ONE_POINT * a1 + self * b1 388 | 389 | # Hare makes two steps 390 | p2, a2, b2 = step(p2, a2, b2) 391 | # assert p2 == ONE_POINT * a2 + self * b2 392 | p2, a2, b2 = step(p2, a2, b2) 393 | # assert p2 == ONE_POINT * a2 + self * b2 394 | 395 | # Hare catches up to Tortoise (in cycle) 396 | if p1 == p2: 397 | break 398 | 399 | # Unlikely case where we have to try different initialization 400 | if b1 == b2: 401 | continue 402 | 403 | k: Scalar = (a2 - a1) * (b1 - b2).reciprocal() 404 | return k 405 | 406 | raise ArithmeticError 407 | 408 | @classmethod 409 | def nth(cls, n: int) -> "AffinePoint": 410 | """ 411 | Return the n-th point on the curve. 412 | 413 | The integer n is internally scaled to the size of the curve. 414 | """ 415 | return ONE_POINT * Scalar.nth(n) 416 | 417 | @classmethod 418 | def random(cls) -> "AffinePoint": 419 | """ 420 | Return a uniformly random point on the curve. 421 | """ 422 | return ONE_POINT * Scalar(random.randrange(NUMBER_POINTS)) 423 | 424 | @classmethod 425 | def sample_greater_one(cls, n_sample: int) -> "List[AffinePoint]": 426 | """ 427 | Randomly sample distinct points on the curve that are greater than one (not zero and not one). 428 | """ 429 | return [ONE_POINT * Scalar(i) for i in random.sample(range(2, NUMBER_POINTS), n_sample)] 430 | 431 | 432 | ZERO_POINT = AffinePoint(None, None) 433 | """ 434 | Zero-point 435 | """ 436 | NUMBER_POINTS = 13 437 | """ 438 | Total number of points on the curve 439 | """ 440 | 441 | 442 | class TestAffinePoint(unittest.TestCase): 443 | def test_double(self): 444 | points = RandomPoints() 445 | one = points.next() 446 | p = ZERO_POINT 447 | 448 | for _ in range(NUMBER_POINTS): 449 | self.assertTrue(p.is_on_curve()) 450 | two_p = p.double() 451 | self.assertTrue(two_p.is_on_curve()) 452 | self.assertEqual(two_p, p + p) 453 | 454 | if two_p.is_zero(): 455 | self.assertEqual(two_p, ZERO_POINT) 456 | if two_p == -two_p: 457 | self.assertEqual(two_p, ZERO_POINT) 458 | 459 | p += one 460 | 461 | # p finished cycle through curve 462 | self.assertEqual(p, ZERO_POINT) 463 | 464 | # XXX: Expensive test 465 | def test_add(self): 466 | points = RandomPoints() 467 | one = points.next() 468 | p = ZERO_POINT 469 | 470 | for _ in range(NUMBER_POINTS): 471 | self.assertTrue(p.is_on_curve()) 472 | q = ZERO_POINT 473 | 474 | for _ in range(NUMBER_POINTS): 475 | self.assertTrue(q.is_on_curve()) 476 | p_plus_q = p + q 477 | self.assertTrue(p_plus_q.is_on_curve()) 478 | 479 | if p.is_zero(): 480 | self.assertEqual(p_plus_q, q) 481 | if q.is_zero(): 482 | self.assertEqual(p_plus_q, p) 483 | if p == -q: 484 | self.assertEqual(p_plus_q, ZERO_POINT) 485 | 486 | q += one 487 | 488 | # q finished cycle through curve 489 | self.assertEqual(q, ZERO_POINT) 490 | 491 | p += one 492 | 493 | # p finished cycle through curve 494 | self.assertEqual(p, ZERO_POINT) 495 | 496 | def test_negation(self): 497 | p = ZERO_POINT 498 | 499 | for _ in range(NUMBER_POINTS): 500 | self.assertTrue(p.is_on_curve()) 501 | minus_p = -p 502 | self.assertTrue(minus_p.is_on_curve()) 503 | self.assertEqual(p + minus_p, ZERO_POINT) 504 | 505 | p += ONE_POINT 506 | 507 | # p finished cycle through curve 508 | self.assertEqual(p, ZERO_POINT) 509 | 510 | # XXX: Very expensive test 511 | def test_scalar_mul(self): 512 | p = ZERO_POINT 513 | 514 | for _ in range(NUMBER_POINTS): 515 | self.assertTrue(p.is_on_curve()) 516 | 517 | for j in range(NUMBER_POINTS): 518 | scalar_j = Scalar(j) 519 | p_times_j = p * scalar_j 520 | self.assertTrue(p_times_j.is_on_curve()) 521 | 522 | p_plus_dot_dot_dot_plus_p = ZERO_POINT 523 | 524 | for _ in range(j): 525 | p_plus_dot_dot_dot_plus_p += p 526 | 527 | self.assertEqual(p_times_j, p_plus_dot_dot_dot_plus_p) 528 | 529 | p += ONE_POINT 530 | 531 | # p finished cycle through curve 532 | self.assertEqual(p, ZERO_POINT) 533 | 534 | def test_discrete_log(self): 535 | p = ZERO_POINT 536 | 537 | for _ in range(NUMBER_POINTS): 538 | self.assertTrue(p.is_on_curve()) 539 | k = p.discrete_log() 540 | self.assertEqual(p, ONE_POINT * k) 541 | 542 | p += ONE_POINT 543 | 544 | # p finished cycle through curve 545 | self.assertEqual(p, ZERO_POINT) 546 | 547 | 548 | def int_from_bytes(b: bytes) -> int: 549 | return int.from_bytes(b, byteorder="big") 550 | 551 | 552 | def reset_one_point(point: AffinePoint): 553 | """ 554 | Set the one-point to the given point. 555 | 556 | **Warning: This changes the definition of scalar multiplication and the discrete logarithm!** 557 | These methods will return different outputs for different definitions of the one-point. 558 | All previous results are invalid for a different one-point. 559 | """ 560 | global ONE_POINT 561 | ONE_POINT = point 562 | 563 | 564 | def number_points() -> Optional[int]: 565 | """ 566 | Return the number of points on the curve. 567 | """ 568 | if ONE_POINT is None: 569 | return None 570 | 571 | minus_one_point = -ONE_POINT 572 | k = minus_one_point.discrete_log() 573 | assert ONE_POINT * k == minus_one_point 574 | assert ONE_POINT + minus_one_point == ZERO_POINT 575 | assert minus_one_point + ONE_POINT == ZERO_POINT 576 | return k.value + 1 577 | 578 | 579 | class TestNumberPoints(unittest.TestCase): 580 | def test_number_points(self): 581 | i = 0 582 | tmp = ZERO_POINT 583 | 584 | while True: 585 | tmp += ONE_POINT 586 | i += 1 587 | 588 | if tmp.is_zero(): 589 | self.assertEqual(NUMBER_POINTS, i) 590 | break 591 | 592 | self.assertEqual(NUMBER_POINTS, number_points()) 593 | 594 | 595 | class RandomPoints: 596 | """ 597 | Deterministic sequence of random non-zero curve points. 598 | 599 | Always returns the same points in the same order. 600 | 601 | Use a seed to jump ahead in the sequence and generate different points. 602 | """ 603 | index: int 604 | max_sub_index = 1000 605 | 606 | def __init__(self): 607 | self.index = 0 608 | 609 | def seed(self, n: int): 610 | """ 611 | Jump ahead in the sequence by n steps. 612 | 613 | Let A be the n-th point in the sequence with seed zero (default). 614 | Let B be the 1-st point in the sequence with seed n - 1. 615 | Then A and B are the same. 616 | """ 617 | self.index = n * self.max_sub_index 618 | 619 | def next(self) -> "AffinePoint": 620 | """ 621 | Return random non-zero curve point. 622 | """ 623 | for sub_index in range(self.max_sub_index): 624 | h = hashlib.sha256((self.index + sub_index).to_bytes(2, byteorder='big')).digest() 625 | x = Coordinate(int_from_bytes(h)) 626 | p = x.lift_x() 627 | 628 | if p is None: 629 | continue 630 | 631 | self.index += self.max_sub_index 632 | return p 633 | 634 | raise StopIteration 635 | 636 | 637 | class TestRandomPoints(unittest.TestCase): 638 | def test_next(self): 639 | points = RandomPoints() 640 | first = points.next() 641 | self.assertFalse(first.is_zero()) 642 | self.assertTrue(first.is_on_curve()) 643 | 644 | second = points.next() 645 | self.assertFalse(second.is_zero()) 646 | self.assertTrue(second.is_on_curve()) 647 | self.assertNotEqual(first, second) 648 | 649 | another_points = RandomPoints() 650 | another_first = another_points.next() 651 | self.assertEqual(first, another_first) 652 | 653 | def test_seed(self): 654 | points = RandomPoints() 655 | for _ in range(10): 656 | points.next() 657 | tenth = points.next() 658 | 659 | another_points = RandomPoints() 660 | another_points.seed(10) 661 | first = another_points.next() 662 | 663 | self.assertEqual(tenth, first) 664 | 665 | 666 | GLOBAL_POINTS = RandomPoints() 667 | """ 668 | Global sequence of random non-zero curve points. 669 | """ 670 | ONE_POINT = GLOBAL_POINTS.next() 671 | """ 672 | Global one-point. 673 | """ 674 | 675 | 676 | class Scalar(ModInt): 677 | """ 678 | Curve scalar. 679 | """ 680 | modulus = NUMBER_POINTS 681 | -------------------------------------------------------------------------------- /graph/three_coloring.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "55fa22e3", 6 | "metadata": {}, 7 | "source": [ 8 | "# Graph three-colorability\n", 9 | "\n", 10 | "In this chapter we construct a zero-knowledge protocol around graph colorability.\n", 11 | "\n", 12 | "This chapter was inspired by [a video by Numberphile](https://www.youtube.com/watch?v=5ovdoxnfFVc) and is based on [a lecture from the Max Plack Institute for Informatics](https://resources.mpi-inf.mpg.de/departments/d1/teaching/ss13/gitcs/lecture9.pdf)." 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "da94a8c5", 18 | "metadata": {}, 19 | "source": [ 20 | "# What is a graph?\n", 21 | "\n", 22 | "[A graph](https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)) consists of nodes and edges. Nodes are points in space. Edges are bridges between nodes.\n", 23 | "\n", 24 | "# What is a (three-)coloring?\n", 25 | "\n", 26 | "**Coloring** a graph means to assign a color to each node. For each edge, adjacent nodes must have a different color.\n", 27 | "\n", 28 | "A coloring comes with a number of colors. How many colors we have available determines how hard it is to color a graph.\n", 29 | "\n", 30 | "For instance, [it was proven that every graph can be four-colored](https://en.wikipedia.org/wiki/Four_color_theorem). In contrast, not every graph can be three-colored! In fact, three-coloring is an NP-complete problem. This means that for large graphs it may take exponential time to three-color them.\n", 31 | "\n", 32 | "A graph is **three-colorable** if there is a three-coloring for it." 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "id": "b60c08d3", 38 | "metadata": {}, 39 | "source": [ 40 | "# What are we proving?\n", 41 | "\n", 42 | "Peggy and Victor are engaged in an interactive proof.\n", 43 | "\n", 44 | "There is a graph.\n", 45 | "\n", 46 | "Peggy thinks she knows a three-coloring of the graph. She wants to prove that to Victor, without revealing the coloring.\n", 47 | "\n", 48 | "Victor is sceptical and wants to see evidence. He wants to expose Peggy as a liar if her coloring is invalid.\n", 49 | "\n", 50 | "Peggy wins if she convinces Victor. Victor wins by accepting only valid three-colorings." 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "id": "f3e89ce9", 56 | "metadata": {}, 57 | "source": [ 58 | "# Jupyter setup\n", 59 | "\n", 60 | "Run the following snippet to set up your Jupyter notebook for the workshop." 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "id": "1a927c2b", 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "import os\n", 71 | "import sys\n", 72 | "\n", 73 | "# Add project root so we can import local modules\n", 74 | "root_dir = sys.path.append(\"..\")\n", 75 | "sys.path.append(root_dir)\n", 76 | "\n", 77 | "# Import here so cells don't depend on each other\n", 78 | "from IPython.display import display\n", 79 | "from typing import List, Tuple, Dict\n", 80 | "import ipywidgets as widgets\n", 81 | "import random\n", 82 | "import networkx as nx\n", 83 | "import matplotlib.pyplot as plt\n", 84 | "\n", 85 | "from local.graph import Mapping, three_colorable_graph, not_three_colorable_graph\n", 86 | "from local.ec.static import CurvePoint, Scalar, ONE_POINT\n", 87 | "from local.ec.util import Opening\n", 88 | "import local.stats as stats" 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "id": "ac9f9934", 94 | "metadata": {}, 95 | "source": [ 96 | "# Select the scenario\n", 97 | "\n", 98 | "Choose the good or the evil scenario. See how it affects the other cells further down.\n", 99 | "\n", 100 | "1. **Peggy is honest** 😇 She knows a three-coloring of the graph. She wants to convince Victor of a true statement.\n", 101 | "2. **Peggy is lying** 😈 She doesn't know a three-coloring of the graph! She tries to fool Victor into believing a false statement.\n", 102 | "\n", 103 | "Also select the **size of the graphs**." 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": null, 109 | "id": "71cde58a", 110 | "metadata": {}, 111 | "outputs": [], 112 | "source": [ 113 | "def generate_coloring(values: Dict):\n", 114 | " global graph, coloring\n", 115 | "\n", 116 | " if honest_dropdown.value:\n", 117 | " # Good: Valid three-coloring\n", 118 | " graph, coloring = three_colorable_graph(n_nodes_slider.value)\n", 119 | " else:\n", 120 | " # Evil: Invalid three-coloring\n", 121 | " graph, coloring = not_three_colorable_graph(n_nodes_slider.value)\n", 122 | "\n", 123 | "honest_dropdown = widgets.Dropdown(\n", 124 | " options=[\n", 125 | " (\"Peggy can three-color 😇\", True),\n", 126 | " (\"Peggy cannot three-color 😈\", False)],\n", 127 | " value=True,\n", 128 | " description=\"Scenario:\",\n", 129 | ")\n", 130 | "honest_dropdown.observe(generate_coloring, names=\"value\")\n", 131 | "\n", 132 | "n_nodes_slider = widgets.IntSlider(min=4, max=20, value=4, step=1, description=\"#Nodes\")\n", 133 | "n_nodes_slider.observe(generate_coloring, names=\"value\")\n", 134 | "\n", 135 | "# Generate default values\n", 136 | "generate_coloring({})\n", 137 | "# Display selection\n", 138 | "display(honest_dropdown)\n", 139 | "display(n_nodes_slider)" 140 | ] 141 | }, 142 | { 143 | "cell_type": "markdown", 144 | "id": "af5ee585", 145 | "metadata": {}, 146 | "source": [ 147 | "# Visualize your graph\n", 148 | "\n", 149 | "Visualize the graph you generated.\n", 150 | "\n", 151 | "You will see the colors in the plot." 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "id": "827dbcf8", 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "node_colors = [coloring[node] for node in graph.nodes]\n", 162 | "nx.draw(graph, node_color=node_colors, with_labels=True)\n", 163 | "plt.show()" 164 | ] 165 | }, 166 | { 167 | "cell_type": "markdown", 168 | "id": "6612cfd9", 169 | "metadata": {}, 170 | "source": [ 171 | "# How the proof goes\n", 172 | "\n", 173 | "Victor knows the graph. Peggy knows a three-coloring of the graph.\n", 174 | "\n", 175 | "1. Peggy randomly shuffles her colors.\n", 176 | "1. Peggy sends commitments to the (shuffled) color of each node to Victor.\n", 177 | "1. Victor selects a random edge.\n", 178 | "1. Peggy opens the commitments of the colors of both nodes of the edge.\n", 179 | "1. Victor checks if the colors are different.\n", 180 | "\n", 181 | "# Why Peggy shuffles colors\n", 182 | "\n", 183 | "Step 1 prevents Victor from knowing Peggy's coloring. Victor sees only one edge which is not enough information to derive the entire coloring. Peggy shuffles differently each time, so Victor cannot learn across multiple rounds.\n", 184 | "\n", 185 | "Each three-coloring gives rise to six variants where the colors are shuffled around. You see the original colors (header) and the possible variants (lines) in the table below.\n", 186 | "\n", 187 | "| red | blue | green |\n", 188 | "|-------|-------|-------|\n", 189 | "| red | blue | green |\n", 190 | "| red | green | blue |\n", 191 | "| blue | red | green |\n", 192 | "| blue | green | red |\n", 193 | "| green | red | blue |\n", 194 | "| green | blue | red |\n", 195 | "\n", 196 | "# Why Peggy commits to all colors\n", 197 | "\n", 198 | "Peggy commits to the (shuffled) color of each edge. If there is an invalid edge where both nodes have the same color, then Victor has a chance of randomly selecting this edge. Peggy is forced to open the commitment to the correct value, which she sends to Victor. He sees that the colors are the same and rejects her coloring proof." 199 | ] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "execution_count": null, 204 | "id": "d02fa337", 205 | "metadata": {}, 206 | "outputs": [], 207 | "source": [ 208 | "punto_uno, = CurvePoint.sample_greater_one(1)\n", 209 | "\n", 210 | "Edge = Tuple[int, int]\n", 211 | "ColoredEdge = Tuple[Opening, Opening]\n", 212 | "\n", 213 | "class Peggy:\n", 214 | " def __init__(self, coloring: Dict[int, int]):\n", 215 | " self.coloring = Mapping(coloring)\n", 216 | " \n", 217 | " def commit(self) -> List[CurvePoint]:\n", 218 | " shuffle = Mapping.shuffle_list([0, 1, 2])\n", 219 | " shuffled_coloring = self.coloring.and_then(shuffle)\n", 220 | " \n", 221 | " self.openings = [Opening(Scalar(shuffled_coloring[index]), ONE_POINT, punto_uno)\n", 222 | " for index in self.coloring]\n", 223 | " commitments = [opening.close() for opening in self.openings]\n", 224 | " \n", 225 | " return commitments\n", 226 | " \n", 227 | " def color_edge(self, edge: Edge) -> ColoredEdge:\n", 228 | " return self.openings[edge[0]], self.openings[edge[1]]\n", 229 | "\n", 230 | "\n", 231 | "class Victor:\n", 232 | " def __init__(self, graph: nx.Graph):\n", 233 | " self.edges = sorted(graph.edges())\n", 234 | "\n", 235 | " def random_edge(self, commitments: List[CurvePoint]) -> Edge:\n", 236 | " self.commitments = commitments\n", 237 | " self.edge = random.choice(self.edges)\n", 238 | " \n", 239 | " return self.edge\n", 240 | " \n", 241 | " def verify(self, colored_edge: ColoredEdge) -> bool:\n", 242 | " # Invalid coloring\n", 243 | " if colored_edge[0].value() == colored_edge[1].value():\n", 244 | " return False\n", 245 | " # Opened colors correspond to commitments\n", 246 | " if not colored_edge[0].verify(self.commitments[self.edge[0]]):\n", 247 | " return False\n", 248 | " if not colored_edge[1].verify(self.commitments[self.edge[1]]):\n", 249 | " return False\n", 250 | " \n", 251 | " return True" 252 | ] 253 | }, 254 | { 255 | "cell_type": "markdown", 256 | "id": "658e60f0", 257 | "metadata": {}, 258 | "source": [ 259 | "# Run the proof\n", 260 | "\n", 261 | "Let's see the proof in action.\n", 262 | "\n", 263 | "Run the Python code below and see what happens.\n", 264 | "\n", 265 | "The outcome depends on the scenario you picked. The outcome is also randomly different each time.\n", 266 | "\n", 267 | "Feel free to run the code multiple times!" 268 | ] 269 | }, 270 | { 271 | "cell_type": "code", 272 | "execution_count": null, 273 | "id": "5e795362", 274 | "metadata": {}, 275 | "outputs": [], 276 | "source": [ 277 | "peggy = Peggy(coloring)\n", 278 | "victor = Victor(graph)\n", 279 | "\n", 280 | "commitments = peggy.commit()\n", 281 | "print(\"Node color commitments: {}\".format(commitments))\n", 282 | "\n", 283 | "edge = victor.random_edge(commitments)\n", 284 | "print(\"Edge: {}\".format(edge))\n", 285 | "\n", 286 | "colored_edge = peggy.color_edge(edge)\n", 287 | "print(\"Colored edge: {}\".format(colored_edge))\n", 288 | "print()\n", 289 | "\n", 290 | "if victor.verify(colored_edge):\n", 291 | " if honest_dropdown.value:\n", 292 | " print(\"Victor is convinced 👌 (expected)\")\n", 293 | " else:\n", 294 | " print(\"Victor is convinced 👌 (Victor was fooled)\")\n", 295 | "else:\n", 296 | " if honest_dropdown.value:\n", 297 | " print(\"Victor is not convinced... 🤨 (Peggy was dumb)\")\n", 298 | " else:\n", 299 | " print(\"Victor is not convinced... 🤨 (expected)\")" 300 | ] 301 | }, 302 | { 303 | "cell_type": "markdown", 304 | "id": "3abe84ec", 305 | "metadata": {}, 306 | "source": [ 307 | "# How the proof is complete\n", 308 | "\n", 309 | "If Peggy can three-color the graph, then **Victor will always be convinced** by her proof.\n", 310 | "\n", 311 | "This is because Peggy commits to a correct three-coloring. The edge that Victor chooses is always correctly colored.\n", 312 | "\n", 313 | "Let's run a couple of exchanges and see how they go." 314 | ] 315 | }, 316 | { 317 | "cell_type": "code", 318 | "execution_count": null, 319 | "id": "146e28c6", 320 | "metadata": {}, 321 | "outputs": [], 322 | "source": [ 323 | "n_exchanges_complete_slider = widgets.IntSlider(min=10, max=1000, value=10, step=10, description=\"#Exchanges\")\n", 324 | "n_exchanges_complete_slider" 325 | ] 326 | }, 327 | { 328 | "cell_type": "code", 329 | "execution_count": null, 330 | "id": "50735e13", 331 | "metadata": {}, 332 | "outputs": [], 333 | "source": [ 334 | "# Good scenario:\n", 335 | "# Peggy knows a valid three-coloring\n", 336 | "graph2, coloring2 = three_colorable_graph(n_nodes_slider.value)\n", 337 | "\n", 338 | "honest_peggy = Peggy(coloring2)\n", 339 | "victor = Victor(graph2)\n", 340 | "\n", 341 | "peggy_success = 0\n", 342 | "\n", 343 | "for _ in range(n_exchanges_complete_slider.value):\n", 344 | " commitments = honest_peggy.commit()\n", 345 | " edge = victor.random_edge(commitments)\n", 346 | " colored_edge = honest_peggy.color_edge(edge)\n", 347 | "\n", 348 | " if victor.verify(colored_edge):\n", 349 | " peggy_success += 1\n", 350 | " \n", 351 | "peggy_success_rate = peggy_success / n_exchanges_complete_slider.value * 100\n", 352 | "\n", 353 | "print(f\"Running {n_exchanges_complete_slider.value} exchanges.\")\n", 354 | "print(f\"Honest Peggy wins {peggy_success_rate:0.2f}% of the time.\")\n", 355 | "print()\n", 356 | "\n", 357 | "assert peggy_success_rate == 100\n", 358 | "print(\"Peggy always wins if she is honest.\")" 359 | ] 360 | }, 361 | { 362 | "cell_type": "markdown", 363 | "id": "1112d8e3", 364 | "metadata": {}, 365 | "source": [ 366 | "# How the proof is sound\n", 367 | "\n", 368 | "If Peggy cannot three-color the graph, then **Victor has a chance to reject** her proof.\n", 369 | "\n", 370 | "If $u$ out of $v$ edges are correctly colored, then Victor has a $\\frac{u}{v}$ chance of randomly selecting one of these edges. In this case, Peggy can reveal two different colors and pass Victor's test.\n", 371 | "\n", 372 | "The more edges are incorrect, the likelier it becomes for Victor to select an incorrect edge. Still, these probabilities don't look good for Victor.\n", 373 | "\n", 374 | "We can increase Victor's confidence by running the protocol for **multiple rounds**.\n", 375 | "\n", 376 | "This means Peggy randomly shuffles her colors and Victor randomly selects an edge multiple times. Each time, Peggy has to reveal the colors of the selected edge. Victor accepts if Peggy answered correctly **all** time times. However, he rejects if Peggy answers incorrectly **even once**.\n", 377 | "\n", 378 | "The chance that Peggy answers correctly for $n$ rounds, while $u$ out of $v$ edges are correctly colored, is $\\left(\\frac{u}{v}\\right)^n$. Because $u < v$, this decreases exponentially in $n$ and becomes tiny! If Peggy answers correctly, then Victor is confident that she didn't cheat.\n", 379 | "\n", 380 | "Let's run a couple of exchanges and see how they go." 381 | ] 382 | }, 383 | { 384 | "cell_type": "code", 385 | "execution_count": null, 386 | "id": "0d7c578d", 387 | "metadata": {}, 388 | "outputs": [], 389 | "source": [ 390 | "n_exchanges_sound_slider = widgets.IntSlider(min=10, max=1000, value=10, step=10, description=\"#Exchanges\")\n", 391 | "n_rounds_slider = widgets.IntSlider(min=1, max=15, value=1, step=1, description=\"#Rounds\")\n", 392 | "\n", 393 | "display(n_exchanges_sound_slider)\n", 394 | "display(n_rounds_slider)" 395 | ] 396 | }, 397 | { 398 | "cell_type": "code", 399 | "execution_count": null, 400 | "id": "36df0b5c", 401 | "metadata": {}, 402 | "outputs": [], 403 | "source": [ 404 | "# Evil scenario:\n", 405 | "# Peggy doesn't know a valid three-coloring\n", 406 | "graph3, coloring3 = not_three_colorable_graph(n_nodes_slider.value)\n", 407 | "\n", 408 | "lying_peggy = Peggy(coloring3)\n", 409 | "victor = Victor(graph3)\n", 410 | "\n", 411 | "victor_success = 0\n", 412 | "\n", 413 | "for _ in range(n_exchanges_sound_slider.value):\n", 414 | " for _ in range(n_rounds_slider.value):\n", 415 | " commitments = lying_peggy.commit()\n", 416 | " edge = victor.random_edge(commitments)\n", 417 | " colored_edge = lying_peggy.color_edge(edge)\n", 418 | " \n", 419 | " if not victor.verify(colored_edge):\n", 420 | " victor_success += 1\n", 421 | " break\n", 422 | " \n", 423 | "victor_success_rate = victor_success / n_exchanges_sound_slider.value * 100\n", 424 | "\n", 425 | "print(f\"Running {n_exchanges_sound_slider.value} exchanges with {n_rounds_slider.value} rounds each.\")\n", 426 | "print(f\"Victor wins against lying Peggy {victor_success_rate:0.2f}% of the time.\")\n", 427 | "print()\n", 428 | "\n", 429 | "if victor_success_rate < 50:\n", 430 | " print(\"Victor loses quite often for a small number of rounds.\")\n", 431 | "elif victor_success_rate < 90:\n", 432 | " print(\"Victor gains more confidence with each added round.\")\n", 433 | "else:\n", 434 | " print(\"At some point it is basically impossible to fool Victor.\")" 435 | ] 436 | }, 437 | { 438 | "cell_type": "markdown", 439 | "id": "0ffde5ff", 440 | "metadata": {}, 441 | "source": [ 442 | "# How the proof is zero-knowledge\n", 443 | "\n", 444 | "The proof itself looks like random noise. Nothing can be extracted from this noise.\n", 445 | "\n", 446 | "Everything that is sent over the wire is randomized:\n", 447 | "\n", 448 | "1. Peggy sends commitments which look like random points.\n", 449 | "1. Victor randomly selects an edge.\n", 450 | "1. Peggy opens the commitments to the selected edge. The commitments contain a randomly shuffled color and a random blinding factor.\n", 451 | "\n", 452 | "We can replicate this pattern:\n", 453 | "\n", 454 | "1. Select a random edge.\n", 455 | "1. Select a random left color.\n", 456 | "1. Select a random right color that is different from the left color.\n", 457 | "1. Create commitments for the left and right node.\n", 458 | "1. Create commitments to random colors for the remaining nodes.\n", 459 | "\n", 460 | "In the fake transcripts, we select the edge first. Then, we color it. Then, we create the commitments.\n", 461 | "\n", 462 | "By construction, the colors of the selected edge are different.\n", 463 | "\n", 464 | "Let's run a chi-square test to see if the original transcripts are distinguishable from the fake transcripts.\n", 465 | "\n", 466 | "**Try small graphs first!** They require fewer samples than large graphs.\n", 467 | "\n", 468 | "Because of the large number of commitments, **this experiment requires especially many samples (#transcripts)**. Make sure to use small graphs. The code also cheats by making the commitment openings more compact than they actually are." 469 | ] 470 | }, 471 | { 472 | "cell_type": "code", 473 | "execution_count": null, 474 | "id": "baf4f71d", 475 | "metadata": {}, 476 | "outputs": [], 477 | "source": [ 478 | "n_transcripts_slider = widgets.IntSlider(min=1000, max=100000, value=10000, step=1000, description=\"#Transcripts\")\n", 479 | "n_transcripts_slider" 480 | ] 481 | }, 482 | { 483 | "cell_type": "code", 484 | "execution_count": null, 485 | "id": "4f2317d8", 486 | "metadata": { 487 | "scrolled": false 488 | }, 489 | "outputs": [], 490 | "source": [ 491 | "peggy = Peggy(coloring)\n", 492 | "victor = Victor(graph)\n", 493 | "edges = sorted(graph.edges()) # for fake transcripts\n", 494 | "\n", 495 | "def real_transcript() -> Tuple:\n", 496 | " commitments = peggy.commit()\n", 497 | " edge = victor.random_edge(commitments)\n", 498 | " left_open, right_open = peggy.color_edge(edge)\n", 499 | "\n", 500 | " return CurvePoint.batch_serialize(commitments, compact=2), edge, \\\n", 501 | " Opening.batch_serialize((left_open, right_open), compact=2)\n", 502 | "\n", 503 | "\n", 504 | "def fake_transcript() -> Tuple:\n", 505 | " edge = random.choice(edges)\n", 506 | " left_node, right_node = edge\n", 507 | " \n", 508 | " left_color = random.randrange(0, 3)\n", 509 | " if random.random() > 0.5:\n", 510 | " right_color = (left_color + 1) % 3\n", 511 | " else:\n", 512 | " right_color = (left_color - 1) % 3\n", 513 | " \n", 514 | " left_color = Scalar(left_color)\n", 515 | " right_color = Scalar(right_color)\n", 516 | " \n", 517 | " left_open = Opening(left_color, ONE_POINT, punto_uno)\n", 518 | " right_open = Opening(right_color, ONE_POINT, punto_uno)\n", 519 | " commitments = [CurvePoint.random() for _ in range(len(graph.nodes))]\n", 520 | " commitments[left_node] = left_open.close()\n", 521 | " commitments[right_node] = right_open.close()\n", 522 | " \n", 523 | " return CurvePoint.batch_serialize(commitments, compact=2), edge, \\\n", 524 | " Opening.batch_serialize((left_open, right_open), compact=2)\n", 525 | "\n", 526 | "print(\"Real transcript: {}\".format(real_transcript()))\n", 527 | "print(\"Fake transcript: {}\".format(fake_transcript()))\n", 528 | "print()\n", 529 | "\n", 530 | "real_samples = [real_transcript() for _ in range(n_transcripts_slider.value)]\n", 531 | "fake_samples = [fake_transcript() for _ in range(n_transcripts_slider.value)]\n", 532 | "\n", 533 | "null_hypothesis = stats.chi_square_equal(real_samples, fake_samples)\n", 534 | "print()\n", 535 | "\n", 536 | "if null_hypothesis:\n", 537 | " print(\"Real and fake transcripts are the same distribution.\")\n", 538 | " print(\"Victor learns nothing 👌\")\n", 539 | "else:\n", 540 | " print(\"Real and fake transcripts are different distributions.\")\n", 541 | " print(\"Victor might learn something 😧\")\n", 542 | "\n", 543 | "stats.plot_comparison(real_samples, fake_samples, \"real\", \"fake\")" 544 | ] 545 | }, 546 | { 547 | "cell_type": "code", 548 | "execution_count": null, 549 | "id": "3810e3ff", 550 | "metadata": {}, 551 | "outputs": [], 552 | "source": [] 553 | } 554 | ], 555 | "metadata": { 556 | "kernelspec": { 557 | "display_name": "Python 3 (ipykernel)", 558 | "language": "python", 559 | "name": "python3" 560 | }, 561 | "language_info": { 562 | "codemirror_mode": { 563 | "name": "ipython", 564 | "version": 3 565 | }, 566 | "file_extension": ".py", 567 | "mimetype": "text/x-python", 568 | "name": "python", 569 | "nbconvert_exporter": "python", 570 | "pygments_lexer": "ipython3", 571 | "version": "3.10.12" 572 | } 573 | }, 574 | "nbformat": 4, 575 | "nbformat_minor": 5 576 | } 577 | --------------------------------------------------------------------------------