Where all prerequisites converge — from curves to zero knowledge proofs
Agenda
Elliptic curve equation & geometry
Point addition: the "connect and flip" rule
Why EC points form an abelian group
Elliptic curves over finite fields
Cyclic groups & generators on curves
The BN128 curve (Ethereum)
Scalar multiplication & the discrete log problem
Building a ZK proof with py_ecc
Elliptic Curve Equation
y² = x³ + ax + b
An elliptic curve is the set of all (x, y) points satisfying this equation, plus a special "point at infinity."
Different a, b values give different curves
BN128 uses: y² = x³ + 3 (a=0, b=3)
secp256k1 (Bitcoin): y² = x³ + 7 (a=0, b=7)
Set-theoretic view: The curve IS a set. Points are members if they satisfy the equation. The binary operator (point addition) is a function from this set to itself.
Point Addition: Connect and Flip
To add points P and Q on the curve:
Draw a line through P and Q
The line intersects the curve at a third point R'
Reflect R' over the x-axis to get R = P ⊕ Q
Key fact: A line that crosses an elliptic curve at two (non-tangent) points will ALWAYS cross at a third point, unless the line is perfectly vertical.
Vertical lines are the special case — they correspond to adding a point to its inverse, giving the point at infinity.
Point Doubling: P + P
What if both points are the same?
Take the tangent line at P (the derivative)
Find where it intersects the curve again
Reflect over x-axis → result is 2P
Point doubling enables fast scalar multiplication:
To compute 1000P, you only need ~10 doublings + ~10 additions
(binary decomposition: 1000 = 512 + 256 + 128 + 64 + 32 + 8)
# 1000P = 512P + 256P + 128P + 64P + 32P + 8P
# 512P = double P nine times (P → 2P → 4P → ... → 512P)
# Total: ~10 doublings + ~6 additions ≈ 16 operations
# Much better than 999 additions!
Over finite fields, division becomes multiplication by the modular inverse. All operations stay exact!
EC Points Form an Abelian Group
Closed: adding two curve points gives another curve point (or point at infinity)
Associative: (P ⊕ Q) ⊕ R = P ⊕ (Q ⊕ R) — proven algebraically
Identity: the point at infinity O (P ⊕ O = P)
Inverse: inverse of (x, y) is (x, -y) (reflection over x-axis)
Commutative: P ⊕ Q = Q ⊕ P (same line = same intersection)
Two points determine a unique line → one unique third intersection → same result regardless of order. Abelian!
Elliptic Curves over Finite Fields
Instead of real numbers, use arithmetic mod p:
y² ≡ x³ + 3 (mod p)
import libnum
def generate_points(mod):
points = []
for x in range(mod):
y_sq = (x**3 + 3) % mod
if libnum.has_sqrtmod_prime_power(y_sq, mod, 1):
for y in libnum.sqrtmod_prime_power(y_sq, mod, 1):
points.append((x, y))
return points
# y² = x³ + 3 (mod 11) — a small example
points = generate_points(11)
print(f"Points on y² = x³ + 3 (mod 11):")
for p in sorted(points):
print(f" {p}")
print(f"Total: {len(points)} points + point at infinity = {len(points)+1}")
Point Addition over Finite Fields
def ec_add(x1, y1, x2, y2, a, p):
"""Add two points on y² = x³ + ax + b (mod p)"""
if (x1, y1) == (None, None): return (x2, y2)
if (x2, y2) == (None, None): return (x1, y1)
if x1 == x2 and y1 == y2:
# Point doubling
lam = ((3 * x1**2 + a) * pow(2 * y1, -1, p)) % p
elif x1 == x2:
# Vertical line → point at infinity
return (None, None)
else:
# Regular addition
lam = ((y2 - y1) * pow(x2 - x1, -1, p)) % p
x3 = (lam**2 - x1 - x2) % p
y3 = (lam * (x1 - x3) - y1) % p
return (x3, y3)
# Example: y² = x³ + 3 (mod 11)
print(ec_add(4, 10, 7, 7, 0, 11)) # Two points on the curve
Generating the Cyclic Group
# y² = x³ + 3 (mod 11), generator G = (4, 10)
G = (4, 10)
p = 11
point = G
print(f"1G = {point}")
for i in range(2, 14):
point = ec_add(point[0], point[1], G[0], G[1], 0, p)
print(f"{i}G = {point}")
# Output:
# 1G = (4, 10) 7G = (0, 5)
# 2G = (7, 7) 8G = (1, 2)
# 3G = (1, 9) 9G = (7, 4)
# 4G = (0, 6) 10G = (4, 1)
# 5G = (8, 8) 11G = (None, None) ← identity!
# 6G = (2, 0) 12G = (4, 10) ← back to G!
# The group has order 12 (11 points + point at infinity)
Important: Order ≠ Modulus
Field modulus: the prime p we do arithmetic over (11 in our example) Curve order: the number of points on the curve (12 in our example)
The modulus and order are different numbers
Adding the modulus to a scalar gives a different point
Adding the order to a scalar gives the same point (it wraps around)
For BN128: modulus ≈ 2254, order ≈ 2254 (close but different!)
The homomorphism works modulo the curve order, not the field modulus. (a + b) mod order = aG + bG
The BN128 Curve (Ethereum)
from py_ecc.bn128 import G1, multiply, add, eq, neg, curve_order
# The generator point
print(f"G1 = {G1}")
# (1, 2) — a small (x,y) pair!
# But the curve operates over a HUGE field:
field_mod = 21888242871839275222246405745257275088696311157297823662689037894645226208583
print(f"Field modulus bits: {field_mod.bit_length()}") # 254
# The curve order (number of points):
print(f"Curve order bits: {curve_order.bit_length()}") # 254
# These are DIFFERENT numbers!
assert field_mod != curve_order
BN128 is used by Ethereum's ZK proof precompiles (ecAdd, ecMul, ecPairing).
254-bit security → ~2128 security level against best known attacks.
Easy: Given x, compute P = xG (scalar multiplication) Hard: Given P, find x such that xG = P (discrete logarithm)
from py_ecc.bn128 import G1, multiply
# EASY: compute 12345G (instant)
secret = 12345
public_point = multiply(G1, secret)
print(f"12345 × G1 = {public_point}")
# HARD: given public_point, find 12345
# The only known approach for BN128 is essentially brute force
# (or baby-step giant-step, but still infeasible for 254-bit numbers)
# With ~2^254 possible values, checking each would take
# longer than the age of the universe, even at 10^18 ops/sec
This asymmetry is the foundation of elliptic curve cryptography and ZK proofs.
Multiplication = Repeated Addition
There is no "point × point" operation on elliptic curves!
"Scalar multiplication" is just repeated point addition.
from py_ecc.bn128 import G1, multiply, add, eq
# multiply(G1, 5) is really:
# G1 + G1 + G1 + G1 + G1
# Verify:
manual = G1
for _ in range(4):
manual = add(manual, G1)
assert eq(manual, multiply(G1, 5))
print("5G = G+G+G+G+G ✓")
# Algebraic identity still works:
# (a+b)G = aG + bG (because it's all addition)
a, b = 100, 200
assert eq(multiply(G1, a + b), add(multiply(G1, a), multiply(G1, b)))
print(f"({a}+{b})G = {a}G + {b}G ✓")
The Homomorphism in Practice
from py_ecc.bn128 import G1, multiply, add, eq, curve_order
# The core identity: (a + b) mod order ↔ aG + bG
# Works even with enormous numbers that overflow!
a = 2**300 + 21
b = 3**50 + 11
# Path 1: add integers, then map to curve
path1 = multiply(G1, (a + b))
# Path 2: map to curve, then add points
path2 = add(multiply(G1, a), multiply(G1, b))
assert eq(path1, path2)
print("φ(a + b) = φ(a) + φ(b) ✓")
print(f"Even with {(a+b).bit_length()}-bit numbers!")
# Also works with modular reduction
path3 = multiply(G1, (a + b) % curve_order)
assert eq(path1, path3)
print("Modular reduction gives same result ✓")
Point Inverses in Finite Fields
from py_ecc.bn128 import G1, multiply, neg, add, is_inf
field_mod = 21888242871839275222246405745257275088696311157297823662689037894645226208583
# The inverse has the same x but the y is the field inverse
for i in range(1, 4):
point = multiply(G1, i)
inv = neg(point)
print(f"{i}G = ({int(point[0])}, {int(point[1])})")
print(f"-{i}G = ({int(inv[0])}, {int(inv[1])})")
# x values are the same
assert int(point[0]) == int(inv[0])
# y values are additive inverses: y + y' = field_mod
assert int(point[1]) + int(inv[1]) == field_mod
# Adding a point to its inverse gives the identity
assert is_inf(add(point, inv))
print(f"{i}G + (-{i}G) = point at infinity ✓\n")
Encoding Rational Numbers
from py_ecc.bn128 import G1, multiply, add, eq, curve_order
# In a finite field, 1/2 = modular inverse of 2
# This lets us encode "fractions" on the curve!
five_over_two = (5 * pow(2, -1, curve_order)) % curve_order
one_half = pow(2, -1, curve_order)
# 5/2 + 1/2 = 3 in regular math
result = add(multiply(G1, five_over_two), multiply(G1, one_half))
expected = multiply(G1, 3)
assert eq(result, expected)
print("(5/2)G + (1/2)G = 3G ✓")
print("Rational arithmetic works on the curve!")
We can encode rational numbers as field elements using modular inverses.
This extends the homomorphism to rational arithmetic.
Building a ZK Proof
from py_ecc.bn128 import G1, multiply, add, eq
# ====== PROVER ======
# "I know x and y such that x + y = 15"
secret_x = 5
secret_y = 10
# Prover computes and publishes:
A = multiply(G1, secret_x) # xG
B = multiply(G1, secret_y) # yG
claim = 15
proof = (A, B, claim)
# ====== VERIFIER ======
# Verifier receives (A, B, 15) but does NOT know x or y
A, B, claimed_sum = proof
# Compute claimed_sum × G
expected = multiply(G1, claimed_sum)
# Check: A + B == claimed_sum × G?
if eq(add(A, B), expected):
print("✓ VERIFIED: prover knows x, y with x + y = 15")
else:
print("✗ REJECTED: proof is invalid")
ZK Proof: Linear Equations
from py_ecc.bn128 import G1, multiply, add, eq
# Prove: "I know x such that 23x = 161"
# (Solution: x = 7)
# ====== PROVER ======
secret_x = 7
commitment = multiply(G1, secret_x) # xG
# ====== VERIFIER ======
# Verifier knows the equation 23x = 161
# They check: 23 × (xG) == 161G
lhs = multiply(commitment, 23) # 23 × (xG) = 23xG
rhs = multiply(G1, 161) # 161G
if eq(lhs, rhs):
print("✓ VERIFIED: prover knows x such that 23x = 161")
print(" (Verifier doesn't know x = 7!)")
# This works because:
# 23 × (xG) = 23xG = (23x)G = 161G ← by homomorphism
Security Considerations
Our simple proofs are NOT fully zero knowledge!
If an attacker guesses x, they can verify by comparing xG to the commitment.
True ZK requires blinding factors (random masks)
Protocols like Groth16, PLONK add these safeguards
But the core mechanism — homomorphic verification — is what we showed
BN128 provides ~128 bits of security against discrete log
Real ZK proof systems add randomness and multi-round interactions to achieve full zero knowledge. The homomorphism we demonstrated is the foundation they build on.
Treating EC as a Black Box
You don't need to understand the implementation of EC point addition.
You just need to know it follows group rules:
✓ Adding EC points is closed — produces another EC point
✓ Adding EC points is associative
✓ There exists an identity (point at infinity)
✓ Every point has an inverse
✓ The group is cyclic — generated from G
✓ Homomorphism: (a+b)G = aG + bG
This is like using a hash function — you don't need to know SHA-256's internals to use it correctly.
Workshop Series Recap
#
Topic
Key Concept
1
ZK Intro & Complexity
Witnesses, P/NP, circuits
2
Finite Fields
Exact arithmetic mod p, no rounding
3
Set Theory & Algebra
Magma → Semigroup → Monoid → Group
4
Group Theory
Cyclic groups, generators, order
5
Homomorphisms
φ(a□b) = φ(a)■φ(b), one-way maps
6
Elliptic Curves
EC group + homomorphism = ZK proofs
You now have the mathematical foundation for ZK proofs!
Next steps: study Groth16, PLONK, or STARKs with this foundation.
Practice Problems
Using py_ecc, verify that (a+b+c)G = aG + bG + cG for random a, b, c.
Verify associativity: (aG + bG) + cG = aG + (bG + cG) for random values.
Prove you know x such that 7x = 49 using EC commitments (without revealing x).
Generate all points on y² = x³ + 3 (mod 23). How many points are there?
Find a generator for the curve in problem 4 and verify it generates all points.
Challenge: Build a ZK proof for a system of two linear equations: 2x + 3y = 13 and x + y = 5.
What's Next?
R1CS: Rank-1 constraint systems — encoding computations as matrices
QAP: Quadratic arithmetic programs — polynomials from R1CS
Groth16: The most gas-efficient ZK-SNARK for Ethereum