Le Grimoire d'Arithmancie - Les zk-SNARKs Expliqués

« Tout devrait être rendu aussi simple que possible, mais pas plus simple. » — Albus Dumbledore (en se réclamant peut-être un peu d'Einstein)
Niveau requis : savoir compter, additionner, et multiplier. C'est tout. Le reste, on le construit.
Promesse de ce cours : aucune valeur ne sortira d'un chapeau magique. Chaque nombre que vous verrez sera calculé par du code Python qui fonctionne réellement, depuis l'équation de départ jusqu'à la vérification finale.
Avant-propos : Quand les LLMs ont besoin d'un Patronus Cryptographique
Pourquoi les mêmes outils qui protègent Gringotts intéressent désormais les ingénieurs qui déploient des modèles de langage.
Les grands modèles de langage (LLMs) — GPT, Claude, Llama, Mistral — ont radicalement transformé l'économie de l'inférence IA. Aujourd'hui, la majorité des utilisateurs y accèdent via des fournisseurs tiers : des entreprises qui hébergent les poids sur leur propre infrastructure GPU et exposent les sorties via des API. Ce modèle est pratique, mais il repose entièrement sur la confiance accordée au fournisseur.
Or cette confiance est devenue structurellement fragile. Un hébergeur peut, sans que personne ne le détecte : remplacer le modèle annoncé par une version quantifiée moins précise (pour réduire ses coûts de calcul), introduire un fine-tuning non divulgué (pour orienter les réponses), ou modifier discrètement le prompt système. Du côté des données, l'utilisateur qui soumet une requête médicale, juridique ou financière à un LLM cloud ne sait jamais où ces données sont traitées, ni si elles seront réutilisées pour l'entraînement.
Ce double problème — intégrité du calcul et confidentialité des données — est exactement celui que la cryptographie moderne cherche à résoudre pour l'ère des LLMs. Trois familles de techniques sont aujourd'hui à l'avant-garde :
Les preuves à divulgation nulle de connaissance (ZKP / zk-SNARKs) permettent à un fournisseur de prouver qu'il a bien exécuté un modèle précis sur une entrée donnée, sans révéler ni les poids du modèle, ni les données de l'utilisateur. C'est l'objet de l'article qui suit, et le cœur du programme de recherche zkML (Zero-Knowledge Machine Learning).
Le chiffrement homomorphe (FHE) permet d'exécuter des opérations arithmétiques directement sur des données chiffrées : le fournisseur traite le texte de l'utilisateur sans jamais le voir en clair. Des entreprises comme Zama développent des frameworks comme Concrete ML à cet effet, mais la surcharge de calcul reste de plusieurs ordres de grandeur par rapport à l'inférence standard.
Le calcul multipartite sécurisé (MPC) distribue le calcul entre plusieurs nœuds de sorte qu'aucun d'entre eux, individuellement, ne dispose de l'information complète. Applicable à l'entraînement fédéré, il se combine souvent avec ZKP ou FHE pour des garanties renforcées.
L'impératif réglementaire européen — un catalyseur inattendu. Ces enjeux techniques ne sont pas seulement académiques : ils entrent directement en collision avec le calendrier de l'AI Act. Depuis le 2 août 2025, les fournisseurs de modèles d'IA à usage général (GPAI) — c'est-à-dire les grands modèles de langage mis sur le marché européen — sont soumis à des obligations de documentation, de transparence sur les données d'entraînement et, pour les modèles à risque systémique, à des évaluations renforcées. Bientôt, les systèmes d'IA à haut risque devront satisfaire à des exigences encore plus strictes : traçabilité de bout en bout, journalisation des sorties (conservation des logs), supervision humaine effective et fourniture de preuves vérifiables de conformité (Art. 9, 10, 11, 26 et Annexe IV du règlement). Sanctions à la clef : jusqu'à 15 millions d'euros ou 3 % du chiffre d'affaires mondial. Or, la plupart de ces obligations supposent que le déployeur puisse prouver — non pas simplement affirmer — comment le modèle a été utilisé, sur quelles données il a été entraîné et que ses sorties sont bien issues du système déclaré. C'est précisément la promesse des zk-SNARKs : produire des preuves mathématiquement vérifiables sans exposer ni les poids du modèle (propriété intellectuelle du fournisseur) ni les données de l'utilisateur (RGPD). La cryptographie à divulgation nulle n'est donc plus seulement un sujet de recherche en sécurité — elle devient une réponse concrète aux exigences de traçabilité imposées par le régulateur européen.
La convergence de ces trois approches avec les LLMs est la frontière la plus active de la cryptographie appliquée en 2025-2026. L'article que vous vous apprêtez à lire pose les fondations mathématiques des zk-SNARKs : une fois ces fondations comprises, la section d'état de l'art en fin d'article vous montrera comment elles sont projetées sur des transformers à plusieurs milliards de paramètres.
Préambule : Le Pari de Harry à Gringotts
Harry Potter se tient devant le coffre de Godric Gryffondor, dans les profondeurs de Gringotts. La serrure magique est gravée d'une équation :
Harry connaît la solution : x = 3 (vérifiez : 27 + 3 + 5 = 35).
À côté de lui, le gobelin Griphook attend. Son rôle : garder le coffre. Il ne peut laisser entrer Harry que si celui-ci prouve qu'il connaît le code secret — c'est la règle de la banque. Sans vérification, n'importe quel imposteur pourrait prétendre être le propriétaire.
Mais voilà le dilemme : si Harry dit « la solution est 3 », il a bien prouvé qu'il connaît le code… mais maintenant Griphook le connaît aussi. Et Griphook, comme tous les gobelins de Gringotts, a ses propres intérêts. Il reviendra cette nuit pour voler le trésor.
Harry se retrouve face à une contradiction apparente :
- Il doit convaincre Griphook qu'il connaît le code (sinon : pas d'accès).
- Il ne doit pas lui révéler le code (sinon : vol assuré).
Harry veut donc prouver qu'il connaît la solution sans la révéler.
C'est exactement le rôle d'un zk-SNARK :
| Lettre | Signification | Ce que ça veut dire |
|---|---|---|
| zk | Zero-Knowledge | Griphook n'apprend rien sur x |
| S | Succinct | La preuve tient sur un parchemin minuscule |
| N | Non-interactive | Pas de questions-réponses, un seul échange |
| AR | Argument of Knowledge | Impossible de tricher sans connaître x |
Acte I — Les Outils de l'Arithmancien
Avant de bâtir le sortilège, nous devons forger nos outils. Trois outils, pas plus, mais maîtrisés à fond.
Chapitre 1 : Le Monde du Modulo (l'arithmétique « pendule »)
1.1 Qu'est-ce que le modulo ?
Imaginez une horloge à p heures. Si p = 12 (notre horloge habituelle) :
- Il est 10h. Vous ajoutez 5 heures. Il est… 3h. Pas 15h. La position retombe dans l'intervalle 0..11.
C'est ça, le modulo. On écrit :
Et on dit : « 10 + 5 est congru à 3 modulo 12. »
La règle de base : a mod p = le reste de la division de a par p.
17 mod 5 = 2(car 17 = 3×5 + 2)100 mod 7 = 2(car 100 = 14×7 + 2)35 mod 12 = 11(car 35 = 2×12 + 11)
1.2 Pourquoi en a-t-on besoin ?
En cryptographie, on veut travailler avec un nombre fini de valeurs (sinon les calculs explosent vers l'infini). On choisit donc un nombre premier p, et on convient que tous nos calculs vivent dans l'ensemble {0, 1, 2, …, p-1}. Cet ensemble s'appelle un corps fini, noté .
Pour tout ce cours, nous prendrons p = 101.
Pourquoi 101 ? Parce que c'est :
- Premier (donc le « corps » a de bonnes propriétés)
- Petit (vous pouvez vérifier les calculs à la main)
- Assez grand pour que nos polynômes tiennent dedans
1.3 Le défi du modulo : la division
Addition mod p : facile (5 + 7 mod 12 = 0). Multiplication mod p : facile (4 × 4 mod 12 = 4). Division mod p : embêtant.
Comment calculer « 1 / 4 mod 12 » ? Il n'y a pas de virgule autorisée ! On cherche un nombre x tel que 4 × x ≡ 1 (mod 12). Mais aucun nombre ne marche (essayez !).
Heureusement, si p est premier, l'inverse existe toujours. On l'appelle l'inverse modulaire et on le note a⁻¹.
Exemple concret : trouver 4⁻¹ mod 101. On cherche x tel que 4x ≡ 1 (mod 101).
On peut tester à la main : 4 × 76 = 304 = 3 × 101 + 1. Donc 4⁻¹ ≡ 76 (mod 101).
L'algorithme qui trouve ça en un éclair s'appelle l'algorithme d'Euclide étendu. Voici notre première brique Python :
P = 101 # Notre univers : tous les calculs se font mod 101
def egcd(a, b):
"""Algorithme d'Euclide étendu. Renvoie (pgcd, x, y) tels que a*x + b*y = pgcd."""
if a == 0:
return (b, 0, 1)
g, x, y = egcd(b % a, a)
return (g, y - (b // a) * x, x)
def modinv(a, p=P):
"""Calcule l'inverse de a modulo p."""
a = a % p
g, x, _ = egcd(a, p)
if g != 1:
raise Exception(f"Pas d'inverse pour {a} mod {p}")
return x % p
# Test
print(modinv(4)) # → 76
print(modinv(2)) # → 51 (car 2×51 = 102 = 101 + 1)
print((4 * 76) % 101) # → 1, victoire !
À retenir : dans F_101, on peut additionner, soustraire, multiplier ET « diviser » (en multipliant par l'inverse). C'est un vrai terrain de jeu mathématique.
Chapitre 2 : Les Polynômes (un peu plus que des équations)
2.1 C'est quoi, un polynôme ?
Un polynôme, c'est juste une somme de termes où chaque terme est un coefficient multiplié par une puissance d'une variable t. Par exemple :
Ici, les coefficients sont [3, 5, 7] (rangés du plus petit degré au plus grand). En Python, on peut représenter ce polynôme par la liste [3, 5, 7].
Évaluer un polynôme, c'est remplacer t par une valeur :
P(0) = 3P(1) = 3 + 5 + 7 = 15P(2) = 3 + 10 + 28 = 41
2.2 Opérations sur les polynômes
Addition : on ajoute les coefficients un à un.
Multiplication : on développe (chaque terme du premier multiplie chaque terme du deuxième).
Division euclidienne : exactement comme la division des entiers, mais avec des polynômes. On obtient un quotient et un reste.
Exemple : diviser par .
On trouve : .
Le quotient est et le reste est 0. Quand le reste est zéro, on dit que le diviseur divise le polynôme. Retenez cette idée, elle sera centrale.
2.3 La classe Polynôme en Python (notre deuxième brique)
class Poly:
"""Polynôme c_0 + c_1*t + c_2*t^2 + ... à coefficients dans F_p."""
def __init__(self, coeffs):
self.c = [x % P for x in coeffs]
# Retire les zéros de tête (pour avoir le bon degré)
while len(self.c) > 1 and self.c[-1] == 0:
self.c.pop()
def eval(self, x):
"""Évalue le polynôme en x (méthode de Horner, efficace)."""
r = 0
for c in reversed(self.c):
r = (r * x + c) % P
return r
def __add__(self, other):
n = max(len(self.c), len(other.c))
r = [0] * n
for i in range(n):
a = self.c[i] if i < len(self.c) else 0
b = other.c[i] if i < len(other.c) else 0
r[i] = (a + b) % P
return Poly(r)
def __sub__(self, other):
n = max(len(self.c), len(other.c))
r = [0] * n
for i in range(n):
a = self.c[i] if i < len(self.c) else 0
b = other.c[i] if i < len(other.c) else 0
r[i] = (a - b) % P
return Poly(r)
def __mul__(self, other):
r = [0] * (len(self.c) + len(other.c) - 1)
for i, a in enumerate(self.c):
for j, b in enumerate(other.c):
r[i + j] = (r[i + j] + a * b) % P
return Poly(r)
def divmod_poly(self, divisor):
"""Division euclidienne : self = q * divisor + r. Renvoie (q, r)."""
num = list(self.c)
den = list(divisor.c)
if len(num) < len(den):
return Poly([0]), Poly(num)
q_len = len(num) - len(den) + 1
q = [0] * q_len
inv_lead = modinv(den[-1])
for i in range(q_len - 1, -1, -1):
q[i] = (num[i + len(den) - 1] * inv_lead) % P
for j in range(len(den)):
num[i + j] = (num[i + j] - q[i] * den[j]) % P
rem = num[:len(den) - 1] if len(den) > 1 else [0]
if not rem:
rem = [0]
return Poly(q), Poly(rem)
def __repr__(self):
if all(c == 0 for c in self.c):
return "0"
parts = []
for i, c in enumerate(self.c):
if c == 0: continue
if i == 0: parts.append(f"{c}")
elif i == 1: parts.append(f"{c}·t" if c != 1 else "t")
else: parts.append(f"{c}·t^{i}" if c != 1 else f"t^{i}")
return " + ".join(parts)
Mini-test :
p1 = Poly([1, 1]) # 1 + t
p2 = Poly([2, 3]) # 2 + 3t
print(p1 * p2) # → 2 + 5·t + 3·t^2
print((p1 * p2).eval(2)) # → 2 + 10 + 12 = 24
Chapitre 3 : L'Interpolation de Lagrange (le sortilège-clef)
3.1 Le problème
Vous voyez deux points sur une feuille : (1, 5) et (2, 7). Combien de droites passent par ces deux points ? Une seule.
Vous voyez trois points : (1, 0), (2, 3), (3, 8). Combien de paraboles (polynômes de degré 2) passent par ces trois points ? Une seule.
Règle générale : par n points, passe un unique polynôme de degré au plus n-1.
L'interpolation de Lagrange est la recette pour fabriquer ce polynôme.
3.2 La recette (avec un exemple à la main)
Cherchons le polynôme passant par (1, 3) et (2, 5).
Idée : construire deux « briques » de polynômes :
- qui vaut 1 en t=1 et 0 en t=2
- qui vaut 0 en t=1 et 1 en t=2
Alors vaudra bien 3 en t=1 et 5 en t=2.
Comment construire ? On veut qu'il s'annule en t=2 → on met un facteur . On veut qu'il vaille 1 en t=1 → on divise par .
Vérifions : ✓, ✓.
Pareil pour :
Donc :
Vérification : ✓, ✓. 🎉
3.3 La formule générale
Pour n points :
Le polynôme vaut 1 en et 0 partout ailleurs. C'est sa magie.
3.4 Code Python (troisième brique)
def lagrange(xs, ys):
"""Trouve l'unique polynôme passant par les points (xs[i], ys[i])."""
result = Poly([0])
for i in range(len(xs)):
numerator = Poly([1])
denominator = 1
for j in range(len(xs)):
if i == j:
continue
numerator = numerator * Poly([(-xs[j]) % P, 1]) # (t - xs[j])
denominator = (denominator * (xs[i] - xs[j])) % P
scalar = (ys[i] * modinv(denominator)) % P
result = result + (numerator * Poly([scalar]))
return result
# Test : on retrouve P(t) = 2t + 1
print(lagrange([1, 2], [3, 5])) # → 1 + 2·t
Acte II — De l'Équation au Circuit
Nous avons nos outils. Maintenant, transformons l'équation de Harry en quelque chose qu'un ordinateur peut manipuler.
Chapitre 4 : Le « Flattening » (aplatir l'équation)
L'équation contient plusieurs opérations imbriquées. Un ordinateur préfère des étapes élémentaires : une seule opération par ligne.
On décompose donc :
| Étape | Opération | Variable créée | Valeur pour x=3 |
|---|---|---|---|
| Porte 1 | x * x | v1 | 9 |
| Porte 2 | v1 * x | v2 | 27 |
| Porte 3 | v2 + x | v3 | 30 |
| Porte 4 | v3 + 5 | out | 35 |
Illustration du circuit :
C'est notre circuit arithmétique. Toutes les preuves zk-SNARK partent d'un tel circuit.
Chapitre 5 : Le Témoin (Witness)
Harry doit dévoiler toutes les valeurs qui transitent dans son circuit. C'est le témoin, noté :
Soit 6 nombres, indexés de 0 à 5.
Pourquoi le « 1 » en première position ? Pour pouvoir « sélectionner » des constantes comme le +5 de la porte 4. La constante 5 sera vue comme 5 × W[0] = 5 × 1 = 5. C'est une astuce technique mais essentielle.
NUM_VARS = 6
VAR_NAMES = ["1", "x", "out", "v1", "v2", "v3"]
def compute_witness(x):
v1 = x * x
v2 = v1 * x
v3 = v2 + x
out = v3 + 5
return [1, x, out, v1, v2, v3]
W = compute_witness(3)
print(W) # → [1, 3, 35, 9, 27, 30]
⚠️ Le witness contient le secret. Harry ne va jamais le donner à Griphook tel quel. Le SNARK va le « cuisiner » pour qu'il devienne illisible.
Chapitre 6 : R1CS — Le Registre des Contraintes
6.1 La forme A·B = C
Le R1CS (Rank-1 Constraint System) exige que chaque porte s'écrive sous la forme :
Où sont des vecteurs sélecteurs : ils choisissent quelles variables du witness participent à la porte.
Le « · » est un produit scalaire : on multiplie chaque case du vecteur par la case correspondante du witness, puis on additionne. Par exemple, si et , alors .
6.2 Les 4 portes traduites
Porte 1 : x * x = v1
On veut isoler x à gauche, x à droite, et v1 en résultat.
- → sélectionne
x(case 1) - → sélectionne
x - → sélectionne
v1(case 3)
Vérification : ✓
Porte 2 : v1 * x = v2
- →
v1 - →
x - →
v2
Vérification : ✓
Porte 3 : (v2 + x) * 1 = v3 ← attention, c'est une addition transformée en multiplication par 1
- →
x + v2 - → constante 1
- →
v3
Vérification : ✓
Porte 4 : (v3 + 5) * 1 = out
- →
5·1 + v3(le 5 multiplie la constante en case 0) - → constante 1
- →
out
Vérification : ✓
6.3 Les trois matrices A, B, C
En empilant les 4 vecteurs (un par porte), on obtient une matrice A de 4 lignes × 6 colonnes. Idem pour B et C :
Matrice A (4 portes × 6 variables) :
1 x out v1 v2 v3
G1 : 0 1 0 0 0 0
G2 : 0 0 0 1 0 0
G3 : 0 1 0 0 1 0
G4 : 5 0 0 0 0 1
Matrice B :
1 x out v1 v2 v3
G1 : 0 1 0 0 0 0
G2 : 0 1 0 0 0 0
G3 : 1 0 0 0 0 0
G4 : 1 0 0 0 0 0
Matrice C :
1 x out v1 v2 v3
G1 : 0 0 0 1 0 0
G2 : 0 0 0 0 1 0
G3 : 0 0 0 0 0 1
G4 : 0 0 1 0 0 0
6.4 Code Python
gates = [
{"A": {1: 1}, "B": {1: 1}, "C": {3: 1}}, # x*x = v1
{"A": {3: 1}, "B": {1: 1}, "C": {4: 1}}, # v1*x = v2
{"A": {1: 1, 4: 1}, "B": {0: 1}, "C": {5: 1}}, # (x+v2)*1 = v3
{"A": {0: 5, 5: 1}, "B": {0: 1}, "C": {2: 1}}, # (5+v3)*1 = out
\]
NUM_GATES = len(gates)
def build_matrices(gates, num_vars):
A, B, C = [], [], []
for g in gates:
ar = [0]*num_vars; br = [0]*num_vars; cr = [0]*num_vars
for i, v in g["A"].items(): ar[i] = v
for i, v in g["B"].items(): br[i] = v
for i, v in g["C"].items(): cr[i] = v
A.append(ar); B.append(br); C.append(cr)
return A, B, C
A_mat, B_mat, C_mat = build_matrices(gates, NUM_VARS)
# Vérification automatique
for i in range(NUM_GATES):
a = sum(A_mat[i][j]*W[j] for j in range(NUM_VARS)) % P
b = sum(B_mat[i][j]*W[j] for j in range(NUM_VARS)) % P
c = sum(C_mat[i][j]*W[j] for j in range(NUM_VARS)) % P
print(f"Porte {i+1} : ({a}) * ({b}) = {c} →", (a*b)%P == c)
Sortie réelle :
Porte 1 : (3) * (3) = 9 → True
Porte 2 : (9) * (3) = 27 → True
Porte 3 : (30) * (1) = 30 → True
Porte 4 : (35) * (1) = 35 → True
À ce stade, Harry pourrait envoyer son witness et ses 4 contraintes à Griphook, qui les vérifierait. Mais ce serait long pour une grosse équation (millions de portes en pratique), et surtout, le witness contient le secret. Il faut compresser et chiffrer. C'est le rôle du QAP.
Acte III — La Magie Polynomiale (le QAP)
Chapitre 7 : Pourquoi Passer aux Polynômes ?
Imaginez deux polynômes de degré 100. S'ils sont différents, en combien de points peuvent-ils prendre la même valeur ? Au plus 100. (Théorème classique : un polynôme de degré n a au plus n racines.)
Maintenant, choisissez un point au hasard parmi les 101 valeurs de . Quelle est la probabilité que ces deux polynômes y soient égaux ? Très faible.
C'est le théorème de Schwartz-Zippel. Conséquence stupéfiante :
Au lieu de vérifier 4 contraintes (puis 1 million), il suffit de vérifier UNE égalité de polynômes en UN SEUL point aléatoire. Si elle tient, on est quasiment certains que les polynômes sont identiques partout.
Reste à transformer nos matrices R1CS en polynômes. C'est le QAP (Quadratic Arithmetic Program).
Chapitre 8 : Construction du QAP
8.1 L'idée centrale
Nos matrices A, B, C ont 4 lignes (une par porte). Au lieu de garder une matrice, on va créer un polynôme par colonne (par variable).
Pour la variable x (colonne 1) de la matrice A, on a les valeurs [0, 1, 0, 1] aux portes 1, 2, 3, 4. On va construire un polynôme tel que :
- (valeur à la porte 1)
- ← (oups, je me suis trompé, c'est en fait
[1, 0, 1, 0]— c'est la colonne d'index 1)
Reprenons en regardant la matrice A : la colonne x est [1, 0, 1, 0].
Donc on veut tel que :
C'est exactement le problème de Lagrange du chapitre 3 ! On a 4 points, on cherche le polynôme.
En faisant le calcul (dans ), Python nous donne :
Vérifions à la main : ✓
8.2 On fait pareil pour toutes les colonnes
Notre witness a 6 variables, nos matrices ont 3 (A, B, C), donc on aura 3 × 6 = 18 polynômes au total. C'est ennuyeux à la main, mais Python s'en charge :
xs = [1, 2, 3, 4] # un point par porte
A_polys, B_polys, C_polys = [], [], []
for v in range(NUM_VARS):
A_polys.append(lagrange(xs, [A_mat[g][v] for g in range(NUM_GATES)]))
B_polys.append(lagrange(xs, [B_mat[g][v] for g in range(NUM_GATES)]))
C_polys.append(lagrange(xs, [C_mat[g][v] for g in range(NUM_GATES)]))
Sortie réelle (matrice A) :
| Variable | Colonne dans A | Polynôme |
|---|---|---|
1 (const) | [0,0,0,5] | |
x | [1,0,1,0] | |
out | [0,0,0,0] | |
v1 | [0,1,0,0] | |
v2 | [0,0,1,0] | |
v3 | [0,0,0,1] |
(Les coefficients « bizarres » comme 96 sont les inverses modulaires des dénominateurs de Lagrange. Par exemple, en rationnels devient dans .)
8.3 La combinaison avec le witness
Maintenant, on combine ces 18 polynômes avec le witness pour obtenir 3 gros polynômes , , :
Pourquoi ça marche : aux points , on a pour la porte correspondante. Autrement dit, le polynôme encode toutes les portes en même temps.
Vérification : à , on doit retrouver la valeur . Le polynôme évalué en 1 doit donner 3. Calculons-le numériquement avec Python :
A_t = Poly([0]); B_t = Poly([0]); C_t = Poly([0])
for i in range(NUM_VARS):
A_t = A_t + (A_polys[i] * Poly([W[i]]))
B_t = B_t + (B_polys[i] * Poly([W[i]]))
C_t = C_t + (C_polys[i] * Poly([W[i]]))
print(f"A(t) = {A_t}")
print(f"B(t) = {B_t}")
print(f"C(t) = {C_t}")
Sortie réelle :
A(t) = 43 + 95·t + 89·t^2 + 79·t^3
B(t) = 98 + 44·t + 96·t^2 + 68·t^3
C(t) = 60 + 38·t + 26·t^2 + 87·t^3
Évaluons en :
- ✓
- ✓
- ✓
Et bien sûr . Notre porte 1 est encodée dans les polynômes !
Chapitre 9 : L'Équation Maîtresse et le Quotient H(t)
9.1 La règle d'or
Si Harry n'a pas triché, alors aux 4 points :
(Car ça équivaut à dire que pour chaque porte.)
Définissons :
Si Harry est honnête, s'annule en 1, 2, 3, 4.
Calculons-le avec Python :
P_t = (A_t * B_t) - C_t
print(f"P(t) = {P_t}")
for x in [1,2,3,4]:
print(f"P({x}) = {P_t.eval(x)}")
Sortie :
P(t) = 13 + 54·t + 36·t^2 + 82·t^3 + 98·t^4 + t^5 + 19·t^6
P(1) = 0
P(2) = 0
P(3) = 0
P(4) = 0
🎉 Notre polynôme s'annule bien aux 4 portes !
9.2 Le polynôme cible Z(t)
Un polynôme qui s'annule en 1, 2, 3, 4 a forcément , , , comme facteurs. Posons :
C'est le polynôme cible (« target polynomial »). Il a 4 racines : exactement les positions de nos portes.
Z_t = Poly([1])
for xi in [1, 2, 3, 4]:
Z_t = Z_t * Poly([(-xi) % P, 1])
print(f"Z(t) = {Z_t}")
Sortie : Z(t) = 24 + 51·t + 35·t^2 + 91·t^3 + t^4
(Développez à la main : . Dans , et . Tout est cohérent.)
9.3 Le quotient H(t)
Puisque s'annule là où s'annule, doit être divisible par . Le quotient s'appelle :
C'est l'équation maîtresse du QAP.
H_t, R_t = P_t.divmod_poly(Z_t)
print(f"H(t) = {H_t}")
print(f"Reste = {R_t}")
Sortie :
H(t) = 30 + 90·t + 19·t^2
Reste = 0
Le reste est zéro. Cela signifie que Harry a correctement résolu le circuit. S'il avait triché, le reste aurait été différent de zéro. (On le vérifiera à la fin avec un faux witness.)
9.4 Le bilan jusqu'ici
À ce stade, sans aucune cryptographie, on a déjà :
- Compressé toutes les contraintes du circuit en une seule équation.
- Réduit la vérification à un test de divisibilité de polynômes.
Mais il reste un problème : Harry n'a toujours pas caché son secret. Si on transmet , , tels quels, on peut les évaluer en 1, 2, 3, 4 et retrouver les valeurs du witness, donc .
Il faut maintenant chiffrer.
Acte IV — Le Bouclier Cryptographique
Chapitre 10 : Les Courbes Elliptiques (la cape d'invisibilité)
10.1 Qu'est-ce qu'une courbe elliptique ?
C'est une équation de la forme :
Pour notre exemple, nous prenons et :
Un point sur la courbe est un couple d'entiers qui satisfait l'équation. Par exemple :
- → → ou (car ).
Donc et sont sur la courbe.
⚠️ Confusion à éviter : le « » de l'équation de la courbe n'a rien à voir avec le « » secret de Harry. Ce sont juste deux variables nommées pareil. Pour éviter la confusion, dans ce chapitre, on appellera les coordonnées d'un point .
10.2 Trouver tous les points (avec Python)
def is_on_curve(pt, p=P):
if pt is None: return True # point à l'infini
X, Y = pt
return (Y*Y - X*X*X - 3) % p == 0
def find_points():
pts = []
for X in range(P):
rhs = (X**3 + 3) % P
for Y in range(P):
if (Y*Y) % P == rhs:
pts.append((X, Y))
return pts
pts = find_points()
print(f"Nombre de points : {len(pts)}")
print(f"Premiers points : {pts[:10]}")
Sortie réelle :
Nombre de points : 101
Premiers points : [(1, 2), (1, 99), (3, 38), (3, 63), (6, 44), ...]
Notre courbe a 101 points (plus le « point à l'infini », noté , qui joue le rôle du zéro).
10.3 L'addition de points (la règle géométrique)
Sur une courbe elliptique, on peut « additionner » deux points avec une règle géométrique précise :
- Tracez la droite passant par et .
- Elle coupe la courbe en un troisième point .
- Le résultat est le symétrique de par rapport à l'axe horizontal.
Diagramme conceptuel :
Quand (doublement), on prend la tangente au lieu de la droite.
Les formules (qu'on n'a pas besoin d'apprendre par cœur, mais qu'il faut coder une fois) :
Si : Si :
Puis :
Tout cela modulo p, bien sûr.
10.4 Code Python
def ec_add(P1, P2, p=P):
if P1 is None: return P2
if P2 is None: return P1
X1, Y1 = P1
X2, Y2 = P2
if X1 == X2:
if (Y1 + Y2) % p == 0:
return None # point à l'infini
m = (3 * X1 * X1 * modinv(2 * Y1, p)) % p # tangente (doublement)
else:
m = ((Y2 - Y1) * modinv((X2 - X1) % p, p)) % p # pente
X3 = (m*m - X1 - X2) % p
Y3 = (m*(X1 - X3) - Y1) % p
return (X3, Y3)
def ec_mul(pt, n, p=P):
"""Calcule n * pt par doublements successifs."""
result = None
addend = pt
n = n % p
while n > 0:
if n & 1:
result = ec_add(result, addend, p)
addend = ec_add(addend, addend, p)
n >>= 1
return result
10.5 Choix du générateur G et calcul pas à pas de 2G, 3G…
On choisit un point public, , comme point de départ. Ici, on prend simplement .
Tous les autres points seront générés par addition répétée : , , etc. Calculons ces premiers pas à la main pour que rien ne sorte d'un chapeau.
Calcul de 2G = G + G (doublement)
Quand on additionne un point avec lui-même, on utilise la formule de la tangente :
Avec et (notre courbe) :
La « division par 4 » en arithmétique modulaire se fait en multipliant par l'inverse de 4. On a trouvé plus haut que .
Puis on applique les formules des coordonnées :
, donc , et .
Calcul de 3G = 2G + G
On additionne maintenant deux points différents et . On utilise la pente ordinaire :
et . L'inverse de 34 ? Puisque , on a .
Coordonnées du résultat :
Calcul de 4G = 3G + G (on peut aussi faire 2G + 2G)
On additionne et :
: on cherche tel que . On trouve , donc .
À partir de là, on laisse Python continuer. Le tableau complet des premiers points :
| n | nG |
|---|---|
| 1 | (1, 2) |
| 2 | (68, 74) |
| 3 | (26, 45) |
| 4 | (65, 98) |
| 5 | (12, 32) |
| 6 | (32, 42) |
| 7 | (91, 35) |
G = (1, 2)
for n in range(1, 8):
print(f"{n}G = {ec_mul(G, n)}")
10.6 Le « problème du logarithme discret » (DLP)
Voici le miracle cryptographique :
- Facile : étant donné et un entier , calculer .
- Très difficile : étant donné et un point , retrouver tel que .
C'est comme mélanger des cartes : facile à faire, impossible à inverser.
Donc, si Harry envoie le point à Griphook, ce dernier ne peut pas deviner que c'est le chiffre 5. C'est la « cape d'invisibilité ».
10.7 La propriété magique : homomorphisme additif
Voici LA propriété qui change tout :
Autrement dit, on peut additionner des chiffres masqués sans les démasquer.
Exemple : Harry veut prouver à Griphook que sans révéler ni 2 ni 3.
- Il envoie et .
- Griphook les additionne sur la courbe : il obtient un point .
- Griphook compare à (qu'il calcule lui-même).
- S'ils sont égaux, l'addition est confirmée.
C'est exactement ce qui se passe dans un SNARK, mais avec des polynômes entiers.
Chapitre 11 : Le Trusted Setup (la Coupe de Feu)
11.1 Le besoin
Harry veut prouver l'égalité de polynômes :
D'après Schwartz-Zippel, il suffit de la vérifier en un seul point aléatoire . Mais :
- Si Harry choisit lui-même, il peut tricher (fabriquer un faux qui marche en ).
- Si Griphook choisit , Harry doit lui révéler ses polynômes (donc son secret).
Solution : un tiers de confiance (« trusted setup ») choisit une fois pour toutes, le garde secret, puis détruit la preuve de sa connaissance. On dit qu'il « brûle la cendre » (toxic waste).
11.2 La cérémonie
Le tiers de confiance :
- Choisit un aléatoire dans . (Pour notre exemple, .)
- Calcule les puissances de « cachées » sur la courbe :
(Une puissance pour chaque degré possible de .)
-
Calcule comme clé de vérification pour Griphook.
-
Détruit et toute trace mémorielle (la mémoire de la Pensine est vidée).
Si jamais fuitait, n'importe qui pourrait forger de fausses preuves. C'est pour ça que les setups réels sont des cérémonies multipartites complexes (genre Zcash) où des dizaines de participants combinent leurs morceaux de hasard, et où il suffit qu'un seul d'entre eux ait été honnête pour que tout reste sûr.
11.3 Code Python
tau = 7 # En vrai, aléatoire et détruit immédiatement
PK = [ec_mul(G, pow(tau, i, P)) for i in range(NUM_GATES + 1)]
z_tau = Z_t.eval(tau)
VK_Z = ec_mul(G, z_tau)
Sortie réelle :
τ (toxic waste, sera détruit) = 7
Proving Key (puissances de τ encodées) :
τ^0 * G = (1, 2)
τ^1 * G = (91, 35)
τ^2 * G = (68, 27)
τ^3 * G = (32, 42)
τ^4 * G = (91, 66)
Z(τ) = 57, donc Z(τ)*G = (32, 42)
11.4 Comment Harry évalue à l'aveugle
Harry connaît son polynôme . Il ne connaît pas , mais il a la PK.
Grâce à l'homomorphisme :
Chaque terme à droite peut être calculé avec la PK ! Harry obtient ainsi le point sans jamais connaître ni .
Chapitre 12 : Les Couplages (Pairings) — la dernière pièce
12.1 Le problème restant
Griphook a reçu :
- (Et la VK : )
Il doit vérifier que :
L'addition de points marche (homomorphisme). Mais la multiplication de points ne marche pas naturellement : ne veut rien dire sur la courbe.
12.2 La solution magique : le pairing
Un couplage bilinéaire est une fonction qui prend deux points et renvoie un nombre dans un autre groupe (un « ciel mathématique » différent). Sa propriété fondamentale :
Autrement dit, le pairing transforme une multiplication cachée en une exponentiation.
Du coup, Griphook peut comparer :
Plus précisément, l'équation devient :
Si l'égalité tient, c'est que , donc que le QAP est vérifié.
12.3 Note honnête : on ne va pas coder de vrai pairing
Implémenter un vrai pairing (par exemple l'algorithme de Miller sur la courbe BN254) demanderait plusieurs centaines de lignes et l'arithmétique sur les corps d'extension . Pour ce cours pédagogique, on va simuler la vérification en revenant aux valeurs scalaires (ce que Griphook ne peut pas vraiment faire en réalité, mais qui est mathématiquement équivalent à ce que ferait le pairing).
# Vérification "simulée" : équivalente à ce que ferait un pairing
lhs = (A_tau * B_tau) % P
rhs = (H_tau * z_tau + C_tau) % P
print(f"A(τ)·B(τ) = {lhs}")
print(f"H(τ)·Z(τ) + C(τ) = {rhs}")
Pour notre exemple :
A(τ)·B(τ) = 48 * 53 mod 101 = 19
H(τ)·Z(τ) + C(τ) = 76 * 57 + 30 mod 101 = 4362 + 30 mod 101
= 4392 mod 101 = 19
✓ Les deux sont égaux. Le coffre s'ouvre.
Acte V — Le Grand Œuvre Complet
Chapitre 13 : Le Code Total
Voici, en un seul fichier, TOUT ce que nous avons construit. Aucune valeur n'est pré-calculée : le code part de l'équation et du secret , et déroule la preuve jusqu'à la vérification finale.
"""
============================================================
GRIMOIRE zk-SNARK : Tout calculé depuis l'équation de Harry
Équation : x^3 + x + 5 = 35, secret x = 3
============================================================
"""
P = 101 # Notre corps fini (modulo p)
# ------------------------------------------------------------
# 1. ARITHMETIQUE MODULAIRE
# ------------------------------------------------------------
def egcd(a, b):
if a == 0: return (b, 0, 1)
g, x, y = egcd(b % a, a)
return (g, y - (b // a) * x, x)
def modinv(a, p=P):
a = a % p
g, x, _ = egcd(a, p)
if g != 1:
raise Exception(f"Pas d'inverse pour {a} mod {p}")
return x % p
# ------------------------------------------------------------
# 2. POLYNOMES
# ------------------------------------------------------------
class Poly:
def __init__(self, coeffs):
self.c = [x % P for x in coeffs]
while len(self.c) > 1 and self.c[-1] == 0:
self.c.pop()
def eval(self, x):
r = 0
for c in reversed(self.c):
r = (r * x + c) % P
return r
def __add__(self, other):
n = max(len(self.c), len(other.c))
r = [0] * n
for i in range(n):
a = self.c[i] if i < len(self.c) else 0
b = other.c[i] if i < len(other.c) else 0
r[i] = (a + b) % P
return Poly(r)
def __sub__(self, other):
n = max(len(self.c), len(other.c))
r = [0] * n
for i in range(n):
a = self.c[i] if i < len(self.c) else 0
b = other.c[i] if i < len(other.c) else 0
r[i] = (a - b) % P
return Poly(r)
def __mul__(self, other):
r = [0] * (len(self.c) + len(other.c) - 1)
for i, a in enumerate(self.c):
for j, b in enumerate(other.c):
r[i + j] = (r[i + j] + a * b) % P
return Poly(r)
def divmod_poly(self, divisor):
num = list(self.c)
den = list(divisor.c)
if len(num) < len(den):
return Poly([0]), Poly(num)
q_len = len(num) - len(den) + 1
q = [0] * q_len
inv_lead = modinv(den[-1])
for i in range(q_len - 1, -1, -1):
q[i] = (num[i + len(den) - 1] * inv_lead) % P
for j in range(len(den)):
num[i + j] = (num[i + j] - q[i] * den[j]) % P
rem = num[:len(den) - 1] if len(den) > 1 else [0]
if not rem: rem = [0]
return Poly(q), Poly(rem)
def __repr__(self):
if all(c == 0 for c in self.c): return "0"
parts = []
for i, c in enumerate(self.c):
if c == 0: continue
if i == 0: parts.append(f"{c}")
elif i == 1: parts.append(f"{c}·t" if c != 1 else "t")
else: parts.append(f"{c}·t^{i}" if c != 1 else f"t^{i}")
return " + ".join(parts)
# ------------------------------------------------------------
# 3. LAGRANGE
# ------------------------------------------------------------
def lagrange(xs, ys):
result = Poly([0])
for i in range(len(xs)):
numerator = Poly([1])
denominator = 1
for j in range(len(xs)):
if i == j: continue
numerator = numerator * Poly([(-xs[j]) % P, 1])
denominator = (denominator * (xs[i] - xs[j])) % P
scalar = (ys[i] * modinv(denominator)) % P
result = result + (numerator * Poly([scalar]))
return result
# ------------------------------------------------------------
# 4. CIRCUIT R1CS : x^3 + x + 5 = 35
# ------------------------------------------------------------
NUM_VARS = 6
VAR_NAMES = ["1", "x", "out", "v1", "v2", "v3"]
gates = [
{"A": {1: 1}, "B": {1: 1}, "C": {3: 1}}, # x*x = v1
{"A": {3: 1}, "B": {1: 1}, "C": {4: 1}}, # v1*x = v2
{"A": {1: 1, 4: 1}, "B": {0: 1}, "C": {5: 1}}, # (x+v2)*1 = v3
{"A": {0: 5, 5: 1}, "B": {0: 1}, "C": {2: 1}}, # (5+v3)*1 = out
\]
NUM_GATES = len(gates)
def build_matrices(gates, num_vars):
A, B, C = [], [], []
for g in gates:
ar = [0]*num_vars; br = [0]*num_vars; cr = [0]*num_vars
for i, v in g["A"].items(): ar[i] = v
for i, v in g["B"].items(): br[i] = v
for i, v in g["C"].items(): cr[i] = v
A.append(ar); B.append(br); C.append(cr)
return A, B, C
def compute_witness(x):
v1 = x * x
v2 = v1 * x
v3 = v2 + x
out = v3 + 5
return [1, x, out, v1, v2, v3]
# ------------------------------------------------------------
# 5. COURBE ELLIPTIQUE y^2 = x^3 + 3 mod P
# ------------------------------------------------------------
def is_on_curve(pt, p=P):
if pt is None: return True
X, Y = pt
return (Y*Y - X*X*X - 3) % p == 0
def ec_add(P1, P2, p=P):
if P1 is None: return P2
if P2 is None: return P1
X1, Y1 = P1
X2, Y2 = P2
if X1 == X2:
if (Y1 + Y2) % p == 0: return None
m = (3 * X1 * X1 * modinv(2 * Y1, p)) % p
else:
m = ((Y2 - Y1) * modinv((X2 - X1) % p, p)) % p
X3 = (m*m - X1 - X2) % p
Y3 = (m*(X1 - X3) - Y1) % p
return (X3, Y3)
def ec_mul(pt, n, p=P):
result = None
addend = pt
n = n % p
while n > 0:
if n & 1: result = ec_add(result, addend, p)
addend = ec_add(addend, addend, p)
n >>= 1
return result
# ------------------------------------------------------------
# PROTOCOLE COMPLET
# ------------------------------------------------------------
def main():
# === ETAPE 1 : LE SECRET DE HARRY ===
x_secret = 3
W = compute_witness(x_secret)
print(f"x secret = {x_secret}")
print(f"x^3 + x + 5 = {x_secret**3 + x_secret + 5}")
print(f"Witness W = {W}")
# === ETAPE 2 : R1CS ===
A_mat, B_mat, C_mat = build_matrices(gates, NUM_VARS)
print("\n--- Vérification R1CS ---")
for i in range(NUM_GATES):
a = sum(A_mat[i][j]*W[j] for j in range(NUM_VARS)) % P
b = sum(B_mat[i][j]*W[j] for j in range(NUM_VARS)) % P
c = sum(C_mat[i][j]*W[j] for j in range(NUM_VARS)) % P
print(f"Porte {i+1} : ({a})·({b}) = {c} →", "OK" if (a*b)%P == c else "ECHEC")
# === ETAPE 3 : QAP ===
xs = list(range(1, NUM_GATES+1))
A_polys = [lagrange(xs, [A_mat[g][v] for g in range(NUM_GATES)]) for v in range(NUM_VARS)]
B_polys = [lagrange(xs, [B_mat[g][v] for g in range(NUM_GATES)]) for v in range(NUM_VARS)]
C_polys = [lagrange(xs, [C_mat[g][v] for g in range(NUM_GATES)]) for v in range(NUM_VARS)]
A_t = Poly([0]); B_t = Poly([0]); C_t = Poly([0])
for i in range(NUM_VARS):
A_t = A_t + (A_polys[i] * Poly([W[i]]))
B_t = B_t + (B_polys[i] * Poly([W[i]]))
C_t = C_t + (C_polys[i] * Poly([W[i]]))
print(f"\nA(t) = {A_t}")
print(f"B(t) = {B_t}")
print(f"C(t) = {C_t}")
# === ETAPE 4 : P(t) et Z(t) ===
P_t = (A_t * B_t) - C_t
Z_t = Poly([1])
for xi in xs: Z_t = Z_t * Poly([(-xi) % P, 1])
H_t, R_t = P_t.divmod_poly(Z_t)
print(f"\nP(t) = {P_t}")
print(f"Z(t) = {Z_t}")
print(f"H(t) = {H_t}")
print(f"Reste = {R_t}")
# === ETAPE 5 : TRUSTED SETUP ===
G = (1, 2)
tau = 7
PK = [ec_mul(G, pow(tau, i, P)) for i in range(NUM_GATES + 1)]
z_tau = Z_t.eval(tau)
VK_Z = ec_mul(G, z_tau)
print(f"\n--- Trusted Setup ---")
print(f"τ = {tau} (à détruire)")
print(f"PK : {PK}")
print(f"Z(τ)·G = {VK_Z}")
# === ETAPE 6 : HARRY GÉNÈRE LA PREUVE ===
A_tau = A_t.eval(tau); B_tau = B_t.eval(tau)
C_tau = C_t.eval(tau); H_tau = H_t.eval(tau)
pi_A = ec_mul(G, A_tau); pi_B = ec_mul(G, B_tau)
pi_C = ec_mul(G, C_tau); pi_H = ec_mul(G, H_tau)
print(f"\n--- Preuve de Harry ---")
print(f"π_A = {pi_A}, π_B = {pi_B}, π_C = {pi_C}, π_H = {pi_H}")
# === ETAPE 7 : GRIPHOOK VERIFIE ===
lhs = (A_tau * B_tau) % P
rhs = (H_tau * z_tau + C_tau) % P
print(f"\n--- Vérification ---")
print(f"A(τ)·B(τ) = {lhs}")
print(f"H(τ)·Z(τ) + C(τ) = {rhs}")
print(">>> COFFRE OUVERT <<<" if lhs == rhs else ">>> REFUS <<<")
# === BONUS : LE MENTEUR ===
print(f"\n--- Si Harry ment (x=4 au lieu de 3) ---")
W_fake = [1, 4, 35, 16, 64, 68]
A_t2 = Poly([0]); B_t2 = Poly([0]); C_t2 = Poly([0])
for i in range(NUM_VARS):
A_t2 = A_t2 + (A_polys[i] * Poly([W_fake[i]]))
B_t2 = B_t2 + (B_polys[i] * Poly([W_fake[i]]))
C_t2 = C_t2 + (C_polys[i] * Poly([W_fake[i]]))
P_t2 = (A_t2 * B_t2) - C_t2
_, R_t2 = P_t2.divmod_poly(Z_t)
print(f"Reste : {R_t2}")
print("Imposteur démasqué !" if any(c != 0 for c in R_t2.c) else "...")
if __name__ == "__main__":
main()
Chapitre 14 : L'Exécution Pas à Pas
Voici la sortie réelle du programme, commentée :
Sortie complète
============================================================
ETAPE 1 : Le secret de Harry
============================================================
x secret = 3
Equation : x^3 + x + 5 = 35
Witness W = [1, 3, 35, 9, 27, 30]
[1, x, out, v1, v2, v3]
============================================================
ETAPE 3 : Verification du R1CS porte par porte
============================================================
Porte 1 : (3) * (3) = 9 -> OK
Porte 2 : (9) * (3) = 27 -> OK
Porte 3 : (30) * (1) = 30 -> OK
Porte 4 : (35) * (1) = 35 -> OK
============================================================
ETAPE 4 : Construction du QAP
============================================================
A(t) = 43 + 95·t + 89·t^2 + 79·t^3
B(t) = 98 + 44·t + 96·t^2 + 68·t^3
C(t) = 60 + 38·t + 26·t^2 + 87·t^3
============================================================
ETAPE 5 : Calcul de P(t) = A(t)*B(t) - C(t)
============================================================
P(t) = 13 + 54·t + 36·t^2 + 82·t^3 + 98·t^4 + t^5 + 19·t^6
Verification que P(t) s'annule aux portes :
P(1) = 0
P(2) = 0
P(3) = 0
P(4) = 0
============================================================
ETAPE 6 : Z(t) et H(t)
============================================================
Z(t) = (t-1)(t-2)(t-3)(t-4) = 24 + 51·t + 35·t^2 + 91·t^3 + t^4
H(t) = P(t) / Z(t) = 30 + 90·t + 19·t^2
Reste = 0
=> SUCCES : Harry connait le secret
============================================================
ETAPE 7 : Courbe elliptique
============================================================
Nombre de points : 101
Generateur G = (1, 2)
2*G = (68, 74)
3*G = (26, 45)
4*G = (65, 98)
5*G = (12, 32)
============================================================
ETAPE 8 : Trusted Setup
============================================================
τ = 7 (toxic waste)
τ^0 * G = (1, 2)
τ^1 * G = (91, 35)
τ^2 * G = (68, 27)
τ^3 * G = (32, 42)
τ^4 * G = (91, 66)
Z(τ) = 57, Z(τ)*G = (32, 42)
============================================================
ETAPE 9 : Harry forge la preuve
============================================================
A(τ) = 48, B(τ) = 53, C(τ) = 30, H(τ) = 76
π_A = (26, 56)
π_B = (68, 74)
π_C = (65, 3)
π_H = (18, 49)
============================================================
ETAPE 10 : Griphook verifie
============================================================
A(τ) * B(τ) = 48 * 53 = 19
H(τ) * Z(τ) + C(τ) = 76 * 57 + 30 = 19
>>> LE COFFRE S'OUVRE : preuve valide. Harry connait bien x. <<<
============================================================
ETAPE BONUS : Si Harry ment ?
============================================================
Faux witness : [1, 4, 35, 16, 64, 68]
Reste de la division : 63 + 36·t + 63·t^2 + 40·t^3
Le menteur est demasque : il y a un reste non nul !
Que faut-il en retenir ?
-
Les 4 portes du circuit sont satisfaites par le witness honnête : on a bien , , etc.
-
Le polynôme s'annule en 1, 2, 3, 4. C'est la preuve mathématique que toutes les portes sont respectées simultanément, condensée dans un seul polynôme.
-
est calculé sans reste. C'est ce qui prouve l'honnêteté de Harry : la division de par tombe juste.
-
Au point secret , on a et . L'égalité tient.
-
Si Harry triche (en mettant ), la division laisse un reste non nul, et l'égalité finale ne tiendrait plus.
Chapitre 15 : Récapitulatif Visuel
État de l'Art : zk-SNARKs et Vérification des LLMs (2024–2026)
Ce qui suit documente les avancées scientifiques les plus récentes à la frontière des preuves à divulgation nulle et des grands modèles de langage. Les fondations mathématiques décrites dans l'article ci-dessus sont exactement ce que ces systèmes mettent en œuvre — à une échelle de milliards de paramètres.
1. Le problème fondamental : faire confiance à un LLM en production
Les grands modèles de langage sont massivement déployés via des fournisseurs tiers. Dans cet environnement, deux menaces coexistent : un hébergeur peut remplacer le modèle annoncé par une version dégradée pour réduire ses coûts, et les données sensibles de l'utilisateur transitent en clair sur une infrastructure qu'il ne contrôle pas.
La réponse cryptographique naturelle est la preuve à divulgation nulle appliquée à l'inférence neuronale, un champ désormais connu sous le sigle ZKML (Zero-Knowledge Machine Learning). Sa promesse : permettre à un fournisseur de prouver mathématiquement qu'il a bien exécuté le bon modèle sur les bonnes entrées — sans révéler ni les poids, ni les données.
2. zkLLM : le premier jalon (ACM CCS 2024)
La publication fondatrice du domaine est zkLLM, présentée à ACM CCS 2024 :
zkLLM: Zero Knowledge Proofs for Large Language Models Haochen Sun, Jason Li, Hongyang Zhang — Université de Waterloo. arXiv:2404.16109, 2024. Implémentation CUDA : github.com/jvhs0706/zkllm-ccs2024
Le système introduit deux composants originaux :
tlookup — un argument de lookup parallélisé pour les opérations non-arithmétiques sur tenseurs (ReLU, Softmax, GeLU). Ces fonctions non-linéaires sont le principal obstacle à l'arithmétisation des réseaux de neurones, car les circuits R1CS/QAP ne savent nativement gérer que l'addition et la multiplication. tlookup réduit la communication de 90 % pour ces opérations.
zkAttn — une preuve à divulgation nulle spécialisée pour le mécanisme d'attention des transformers, en particulier la fonction softmax à l'intérieur du produit scalaire pondéré.
Sur LLaMA-2-13B, zkLLM génère une preuve en environ 15 minutes par passe forward, avec une erreur L1 de l'ordre de — comparable à l'erreur introduite par la virgule flottante demi-précision. C'est un premier résultat remarquable, mais encore loin de l'inférence standard (quelques millisecondes).
3. zkGPT : vers la non-interactivité (USENIX Security 2025)
Alors que zkLLM repose sur des preuves interactives (protocole sumcheck), zkGPT attaque le problème différemment :
zkGPT: An Efficient Non-interactive Zero-knowledge Proof Framework for LLM Inference Wenjie Qu, Yijun Sun, Xuanming Liu, Tao Lu, Yanpei Guo, Kai Chen, Jiaheng Zhang — National University of Singapore / HKUST. IACR ePrint 2025/1184. Présenté à USENIX Security 2025.
La non-interactivité est cruciale pour les déploiements réels : une preuve non-interactive (obtenue via la transformation de Fiat-Shamir) peut être générée offline et vérifiée asynchronement — exactement le modèle d'un fournisseur cloud.
zkGPT introduit la fusion de contraintes (constraint fusion) et le circuit squeeze, qui réduisent les frais de calcul de plusieurs ordres de grandeur. Résultat : une preuve pour GPT-2 en moins de 25 secondes, soit une accélération de plus de 185× par rapport aux systèmes non-interactifs précédents.
4. TOPLOC : vérification légère par empreinte numérique (2025)
La génération de preuves ZK reste coûteuse pour les grands modèles. TOPLOC propose une approche orthogonale, plus pragmatique :
TOPLOC: A Locality Sensitive Hashing Scheme for Trustless Verifiable Inference Jack Min Ong et al. — PrimeIntellect AI. arXiv:2501.16007, janvier 2025. Code : github.com/PrimeIntellect-ai/toploc
L'idée, avec une analogie simple : imaginez que vous confiez une recette secrète à un cuisinier et que vous voulez vérifier qu'il a bien suivi votre recette — sans le regarder cuisiner. Une façon de le détecter s'il triche : prendre une « photo thermique » du plat en cours de cuisson à des moments précis. Si quelqu'un remplace la recette ou modifie les ingrédients, la température à ces instants sera différente. Inutile de surveiller chaque geste : quelques relevés suffisent.
TOPLOC fait exactement ça avec un LLM. Au lieu de prouver cryptographiquement chaque calcul (très lent), on génère une empreinte compacte de l'état interne du modèle à certaines étapes de l'inférence. Si le fournisseur utilise un autre modèle, une version trafiquée, ou change la précision des calculs pour économiser de l'argent, l'empreinte sera différente — et la tromperie sera détectée.
Ce que ça donne en pratique : pour 32 tokens générés par LLaMA 3.1-8B, l'empreinte ne pèse que 258 octets — des dizaines de milliers de fois plus légère que de stocker l'état interne brut. La validation est plus rapide que l'inférence elle-même. Et dans les tests empiriques, le taux de détection est de 100 % : zéro faux positif, zéro faux négatif.
Ce que TOPLOC ne fait pas : il ne prouve pas mathématiquement que le résultat est correct — seulement que le bon modèle a bien été utilisé. C'est moins fort que les preuves ZK formelles, mais c'est déployable aujourd'hui, sans attendre que les SNARKs soient assez rapides pour les grands modèles.
5. VeriLLM : inférence décentralisée avec garanties économiques (2025–2026)
Le déploiement d'inférence LLM dans des réseaux permissionless (blockchains, réseaux P2P) soulève un problème de jeu : les nœuds sont rationnellement incités à « tricher » (sauter des calculs ou dégrader la précision) pour réduire leurs coûts. VeriLLM répond à ce défi :
VeriLLM: A Lightweight Framework for Publicly Verifiable Decentralized Inference Wang et al. arXiv:2509.24257, septembre 2025 (mis à jour janvier 2026).
VeriLLM combine une re-vérification empirique légère (environ 1 % du coût d'inférence) avec des vérifications minimales on-chain, dans une architecture isomorphe où les mêmes nœuds GPU jouent alternativement les rôles de prouveur et de vérificateur. Ce design empêche les optimisations sélectives et force l'indiscernabilité des tâches. La sécurité est garantie sous un modèle d'incitation : un nœud triche seulement si c'est économiquement rationnel — VeriLLM rend cela systématiquement non-rationnel.
6. ZKPROV : prouver la provenance des données d'entraînement (2025)
Une lacune criante des approches existantes : elles prouvent que l'inférence s'est bien déroulée, mais pas que le modèle a été entraîné sur les données qu'il prétend avoir utilisées. ZKPROV comble ce vide :
ZKPROV: A Zero-Knowledge Approach to Dataset Provenance for Large Language Models arXiv:2506.20915, juin 2025.
Dans les secteurs régulés (santé, droit, finance) où les données d'entraînement doivent être certifiées conformes (RGPD, HIPAA, AI Act), pouvoir prouver cryptographiquement qu'un modèle a bien été entraîné sur un dataset autorisé — sans révéler ce dataset — représente un enjeu de conformité considérable. C'est précisément ce que ZKPROV adresse.
7. ZK-DeepSeek et la convergence Web3 × LLM (2025)
L'essor de l'IA décentralisée a produit ZK-DeepSeek, une version du modèle DeepSeek entièrement vérifiable par zk-SNARK :
Zero-Knowledge Proof Based Verifiable Inference of Models arXiv:2511.19902, novembre 2025.
Le système utilise la transformation de Fiat-Shamir pour produire des preuves non-interactives à taille constante, et illustre la tendance à intégrer des LLMs vérifiables directement dans des infrastructures Web3 : trading bots confidentiels, oracles d'IA on-chain, systèmes de scoring de crédit préservant la vie privée.
8. Personnalisation privée des LLMs avec ZKP (2025)
Au-delà de la vérification, les ZKP ouvrent la voie à des architectures nouvelles pour la personnalisation des LLMs sans exposition des données utilisateur :
Generating Privacy-Preserving Personalized Advice with Zero-Knowledge Proofs and LLMs Watanabe et al. arXiv:2502.06425, février 2025.
Le principe : un premier service LLM infère les traits de l'utilisateur (profil médical, préférences financières) et génère simultanément une preuve ZK attestant que ces traits ont été calculés correctement. Un second service consomme ces traits prouvés pour personnaliser ses réponses, sans jamais accéder aux données brutes. Cas d'usage direct : chatbots d'aide à la décision médicale où la confidentialité des données de santé est non-négociable.
9. VeriLoRA / zkLoRA : vérification du fine-tuning (2025)
L'entraînement complet d'un LLM en ZKP reste hors de portée — les coûts de computation sont prohibitifs. En revanche, le fine-tuning par adaptation de rang faible (LoRA) est suffisamment léger pour être rendu vérifiable. Plusieurs frameworks ont émergé en 2025, dont VeriLoRA et zkLoRA (Roy et al., 2025), qui permettent de prouver que les mises à jour LoRA ont été appliquées correctement à un modèle de base donné — ouvrant la voie à des marketplaces de fine-tuning certifiés auditables sans révéler les poids adaptateurs.
10. Tableau comparatif des approches
| Système | Type de preuve | Modèle cible | Overhead | Innovation principale | Référence |
|---|---|---|---|---|---|
| zkLLM (2024) | Interactive (sumcheck) | LLaMA-2-13B | ~15 min/passe | tlookup, zkAttn | arXiv:2404.16109 |
| zkGPT (2025) | Non-interactive (SNARK) | GPT-2 | <25 sec | Constraint fusion, circuit squeeze | ePrint:2025/1184 |
| TOPLOC (2025) | LSH (heuristique) | LLaMA 3.1-8B | 258 B / 32 tokens | Empreinte polynomiale des activations | arXiv:2501.16007 |
| VeriLLM (2026) | Re-run léger + on-chain | Général | ~1% du coût | Architecture isomorphe, incitations | arXiv:2509.24257 |
| ZK-DeepSeek (2025) | SNARK non-interactif | DeepSeek | Preuve taille fixe | Fiat-Shamir sur transformer | arXiv:2511.19902 |
| ZKPROV (2025) | ZKP sur datasets | Général | — | Provenance des données d'entraînement | arXiv:2506.20915 |
11. Défis ouverts et horizons 2026
La communauté ZKML identifie plusieurs verrous scientifiques encore non résolus :
Scalabilité mémoire — prouver l'inférence d'un LLM à 70 milliards de paramètres nécessite des designs de type SNARKs récursifs ou folding schemes (Nova, HyperNova, NeutronNova). Ces techniques permettent de décomposer la preuve token par token sans que la taille explose. C'est une piste très active pour 2026 : ResNet-50 passe d'une preuve de 1,27 Go (anciens systèmes) à moins de 100 Ko (systèmes à folding).
Quantification en corps finis — les LLMs opèrent en virgule flottante (float16, bfloat16), tandis que les circuits ZK opèrent sur des corps finis d'entiers. Atteindre une précision sub-16 bits tout en conservant une erreur négligeable est un problème ouvert.
Preuves de training end-to-end — la rétropropagation implique des millions d'étapes interdépendantes. Prouver un entraînement complet par ZKP est aujourd'hui qualifié d'« insurmontable » par les auteurs de zkLLM. Les approches LoRA ou par checkpoints partiels sont des compromis pragmatiques.
Mécanismes d'attention — à peine faisables en 2024, les preuves du mécanisme d'attention multi-têtes progressent grâce à des améliorations des protocoles de lookup (Baloo, Caulk+). En 2026, les transformers devraient devenir des citoyens de première classe dans les frameworks ZKML.
Vitesse de vérification côté client — si la génération de preuve reste le domaine du fournisseur GPU, la vérification doit pouvoir s'exécuter sur des appareils grand public. Les SNARKs non-interactifs (Groth16, PLONK) offrent des preuves compactes (~200 octets) avec vérification en quelques millisecondes.
12. Ressources pour approfondir
Pour les fondations mathématiques, la référence canonique (gratuite) :
Proofs, Arguments, and Zero-Knowledge — Justin Thaler, Georgetown University, 2022.
Pour le panorama ZKML en pratique :
The Definitive Guide to ZKML (2025) — ICME Blog, janvier 2026.
A Survey of Zero-Knowledge Proof Based Verifiable Machine Learning — arXiv:2502.18535, février 2025.
Emergent Mind — Topics: zkLLM — Agrégateur de littérature scientifique, mis à jour en continu.
Zero-Knowledge Proofs for ML Inference — Emergent Mind, juillet 2025.
« La magie, vois-tu, ce n'est pas de tirer un lapin d'un chapeau. C'est de comprendre comment le lapin y est rentré. » — un professeur d'Arithmancie, probablement
Les opinions exprimées dans cet article sont strictement personnelles et ne reflètent pas nécessairement celles de mon employeur. Les contenus sont fournis à titre informatif et ne constituent pas un conseil juridique. Cet article explore des concepts architecturaux émergents et analyse des tendances de marché.