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

61 min de lecture
AIActCryptoGenAIResponsibleAI

insight_zksnarks

« 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 :

x3+x+5=35x^3 + x + 5 = 35

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 :

LettreSignificationCe que ça veut dire
zkZero-KnowledgeGriphook n'apprend rien sur x
SSuccinctLa preuve tient sur un parchemin minuscule
NNon-interactivePas de questions-réponses, un seul échange
ARArgument of KnowledgeImpossible de tricher sans connaître x

insight_zksnarks

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 :

10+53(mod12)10 + 5 \equiv 3 \pmod{12}

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é Fp\mathbb{F}_p.

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 :

P(t)=3+5t+7t2P(t) = 3 + 5t + 7t^2

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) = 3
  • P(1) = 3 + 5 + 7 = 15
  • P(2) = 3 + 10 + 28 = 41

2.2 Opérations sur les polynômes

Addition : on ajoute les coefficients un à un.

(2+3t)+(5+t+4t2)=7+4t+4t2(2 + 3t) + (5 + t + 4t^2) = 7 + 4t + 4t^2

Multiplication : on développe (chaque terme du premier multiplie chaque terme du deuxième).

(1+t)(2+3t)=2+3t+2t+3t2=2+5t+3t2(1 + t)(2 + 3t) = 2 + 3t + 2t + 3t^2 = 2 + 5t + 3t^2

Division euclidienne : exactement comme la division des entiers, mais avec des polynômes. On obtient un quotient et un reste.

Exemple : diviser t31t^3 - 1 par t1t - 1.

On trouve : t31=(t1)(t2+t+1)+0t^3 - 1 = (t - 1)(t^2 + t + 1) + 0.

Le quotient est t2+t+1t^2 + t + 1 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 :

  • L1(t)L_1(t) qui vaut 1 en t=1 et 0 en t=2
  • L2(t)L_2(t) qui vaut 0 en t=1 et 1 en t=2

Alors P(t)=3L1(t)+5L2(t)P(t) = 3 \cdot L_1(t) + 5 \cdot L_2(t) vaudra bien 3 en t=1 et 5 en t=2.

Comment construire L1(t)L_1(t) ? On veut qu'il s'annule en t=2 → on met un facteur (t2)(t - 2). On veut qu'il vaille 1 en t=1 → on divise par (12)=1(1 - 2) = -1.

L1(t)=t212=t21=t+2L_1(t) = \frac{t - 2}{1 - 2} = \frac{t-2}{-1} = -t + 2

Vérifions : L1(1)=1L_1(1) = 1 ✓, L1(2)=0L_1(2) = 0 ✓.

Pareil pour L2(t)L_2(t) :

L2(t)=t121=t1L_2(t) = \frac{t - 1}{2 - 1} = t - 1

Donc :

P(t)=3(t+2)+5(t1)=3t+6+5t5=2t+1P(t) = 3 \cdot (-t + 2) + 5 \cdot (t - 1) = -3t + 6 + 5t - 5 = 2t + 1

Vérification : P(1)=3P(1) = 3 ✓, P(2)=5P(2) = 5 ✓. 🎉

3.3 La formule générale

Pour n points (x1,y1),,(xn,yn)(x_1, y_1), \dots, (x_n, y_n) :

P(t)=i=1nyiLi(t)avecLi(t)=jitxjxixjP(t) = \sum_{i=1}^{n} y_i \cdot L_i(t) \quad \text{avec} \quad L_i(t) = \prod_{j \neq i} \frac{t - x_j}{x_i - x_j}

Le polynôme LiL_i vaut 1 en xix_i 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 x3+x+5=35x^3 + x + 5 = 35 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 :

ÉtapeOpérationVariable crééeValeur pour x=3
Porte 1x * xv19
Porte 2v1 * xv227
Porte 3v2 + xv330
Porte 4v3 + 5out35

Illustration du circuit :

x x × ← Porte 1 v₁ = 9 x × ← Porte 2 v₂ = 27 x + ← Porte 3 v₃ = 30 5 + ← Porte 4 out = 35 ✓

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é WW :

W=[1constante, 3x, 35out, 9v1, 27v2, 30v3]W = [\underbrace{1}_{\text{constante}},\ \underbrace{3}_{x},\ \underbrace{35}_{\text{out}},\ \underbrace{9}_{v_1},\ \underbrace{27}_{v_2},\ \underbrace{30}_{v_3}]

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 :

(AW)×(BW)=(CW)(A \cdot W) \times (B \cdot W) = (C \cdot W)

A,B,CA, B, C 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 A=[0,1,0,0,0,0]A = [0, 1, 0, 0, 0, 0] et W=[1,3,35,9,27,30]W = [1, 3, 35, 9, 27, 30], alors AW=01+13+035+...=3A \cdot W = 0\cdot 1 + 1\cdot 3 + 0\cdot 35 + ... = 3.

6.2 Les 4 portes traduites

Porte 1 : x * x = v1

On veut isoler x à gauche, x à droite, et v1 en résultat.

  • A1=[0,1,0,0,0,0]A_1 = [0, 1, 0, 0, 0, 0] → sélectionne x (case 1)
  • B1=[0,1,0,0,0,0]B_1 = [0, 1, 0, 0, 0, 0] → sélectionne x
  • C1=[0,0,0,1,0,0]C_1 = [0, 0, 0, 1, 0, 0] → sélectionne v1 (case 3)

Vérification : (A1W)×(B1W)=3×3=9=C1W(A_1 \cdot W) \times (B_1 \cdot W) = 3 \times 3 = 9 = C_1 \cdot W

Porte 2 : v1 * x = v2

  • A2=[0,0,0,1,0,0]A_2 = [0, 0, 0, 1, 0, 0]v1
  • B2=[0,1,0,0,0,0]B_2 = [0, 1, 0, 0, 0, 0]x
  • C2=[0,0,0,0,1,0]C_2 = [0, 0, 0, 0, 1, 0]v2

Vérification : 9×3=279 \times 3 = 27

Porte 3 : (v2 + x) * 1 = v3 ← attention, c'est une addition transformée en multiplication par 1

  • A3=[0,1,0,0,1,0]A_3 = [0, 1, 0, 0, 1, 0]x + v2
  • B3=[1,0,0,0,0,0]B_3 = [1, 0, 0, 0, 0, 0] → constante 1
  • C3=[0,0,0,0,0,1]C_3 = [0, 0, 0, 0, 0, 1]v3

Vérification : (3+27)×1=30(3 + 27) \times 1 = 30

Porte 4 : (v3 + 5) * 1 = out

  • A4=[5,0,0,0,0,1]A_4 = [5, 0, 0, 0, 0, 1]5·1 + v3 (le 5 multiplie la constante en case 0)
  • B4=[1,0,0,0,0,0]B_4 = [1, 0, 0, 0, 0, 0] → constante 1
  • C4=[0,0,1,0,0,0]C_4 = [0, 0, 1, 0, 0, 0]out

Vérification : (5+30)×1=35(5 + 30) \times 1 = 35

6.3 Les trois matrices A, B, C

En empilant les 4 vecteurs AiA_i (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 F101\mathbb{F}_{101}. 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 Ax(t)A_x(t) tel que :

  • Ax(1)=0A_x(1) = 0 (valeur à la porte 1)
  • Ax(2)=1A_x(2) = 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 Ax(t)A_x(t) tel que :

  • Ax(1)=1A_x(1) = 1
  • Ax(2)=0A_x(2) = 0
  • Ax(3)=1A_x(3) = 1
  • Ax(4)=0A_x(4) = 0

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 F101\mathbb{F}_{101}), Python nous donne :

Ax(t)=8+56t+5t2+33t3A_x(t) = 8 + 56t + 5t^2 + 33t^3

Vérifions à la main : Ax(1)=8+56+5+33=1021(mod101)A_x(1) = 8 + 56 + 5 + 33 = 102 \equiv 1 \pmod{101}

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) :

VariableColonne dans APolynôme Ai(t)A_i(t)
1 (const)[0,0,0,5]96+26t+96t2+85t396 + 26t + 96t^2 + 85t^3
x[1,0,1,0]8+56t+5t2+33t38 + 56t + 5t^2 + 33t^3
out[0,0,0,0]00
v1[0,1,0,0]95+60t+97t2+51t395 + 60t + 97t^2 + 51t^3
v2[0,0,1,0]4+94t+54t2+50t34 + 94t + 54t^2 + 50t^3
v3[0,0,0,1]100+86t+100t2+17t3100 + 86t + 100t^2 + 17t^3

(Les coefficients « bizarres » comme 96 sont les inverses modulaires des dénominateurs de Lagrange. Par exemple, 5/6-5/6 en rationnels devient 9696 dans F101\mathbb{F}_{101}.)

8.3 La combinaison avec le witness

Maintenant, on combine ces 18 polynômes avec le witness pour obtenir 3 gros polynômes A(t)A(t), B(t)B(t), C(t)C(t) :

A(t)=i=05W[i]Ai(t)A(t) = \sum_{i=0}^{5} W[i] \cdot A_i(t)

B(t)=i=05W[i]Bi(t)B(t) = \sum_{i=0}^{5} W[i] \cdot B_i(t)

C(t)=i=05W[i]Ci(t)C(t) = \sum_{i=0}^{5} W[i] \cdot C_i(t)

Pourquoi ça marche : aux points t=1,2,3,4t = 1, 2, 3, 4, on a A(t)=AWA(t) = A \cdot W pour la porte correspondante. Autrement dit, le polynôme encode toutes les portes en même temps.

Vérification : à t=1t = 1, on doit retrouver la valeur A1W=3A_1 \cdot W = 3. Le polynôme A(t)A(t) é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 t=1t=1 :

  • A(1)=43+95+89+79=3063(mod101)A(1) = 43 + 95 + 89 + 79 = 306 \equiv 3 \pmod{101}
  • B(1)=98+44+96+68=3063(mod101)B(1) = 98 + 44 + 96 + 68 = 306 \equiv 3 \pmod{101}
  • C(1)=60+38+26+87=2119(mod101)C(1) = 60 + 38 + 26 + 87 = 211 \equiv 9 \pmod{101}

Et bien sûr 3×3=93 \times 3 = 9. 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 t=1,2,3,4t = 1, 2, 3, 4 :

A(t)B(t)C(t)=0A(t) \cdot B(t) - C(t) = 0

(Car ça équivaut à dire que AW×BW=CWA \cdot W \times B \cdot W = C \cdot W pour chaque porte.)

Définissons :

P(t)=A(t)B(t)C(t)P(t) = A(t) \cdot B(t) - C(t)

Si Harry est honnête, P(t)P(t) 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 (t1)(t-1), (t2)(t-2), (t3)(t-3), (t4)(t-4) comme facteurs. Posons :

Z(t)=(t1)(t2)(t3)(t4)Z(t) = (t-1)(t-2)(t-3)(t-4)

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 : (t1)(t2)(t3)(t4)=t410t3+35t250t+24(t-1)(t-2)(t-3)(t-4) = t^4 - 10t^3 + 35t^2 - 50t + 24. Dans F101\mathbb{F}_{101}, 1091-10 \equiv 91 et 5051-50 \equiv 51. Tout est cohérent.)

9.3 Le quotient H(t)

Puisque P(t)P(t) s'annule là où Z(t)Z(t) s'annule, P(t)P(t) doit être divisible par Z(t)Z(t). Le quotient s'appelle H(t)H(t) :

A(t)B(t)C(t)=H(t)Z(t)\boxed{A(t) \cdot B(t) - C(t) = H(t) \cdot Z(t)}

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 A(t)A(t), B(t)B(t), C(t)C(t) tels quels, on peut les évaluer en 1, 2, 3, 4 et retrouver les valeurs du witness, donc xx.

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 :

y2=x3+ax+b(modp)y^2 = x^3 + ax + b \pmod{p}

Pour notre exemple, nous prenons a=0a = 0 et b=3b = 3 :

y2=x3+3(mod101)y^2 = x^3 + 3 \pmod{101}

Un point sur la courbe est un couple d'entiers (x,y)(x, y) qui satisfait l'équation. Par exemple :

  • x=1x = 1y2=1+3=4y^2 = 1 + 3 = 4y=2y = 2 ou y=99y = 99 (car 992=9801=97×101+4499^2 = 9801 = 97 \times 101 + 4 \equiv 4).

Donc (1,2)(1, 2) et (1,99)(1, 99) sont sur la courbe.

⚠️ Confusion à éviter : le « xx » de l'équation de la courbe n'a rien à voir avec le « x=3x = 3 » 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 (X,Y)(X, Y).

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é O\mathcal{O}, 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 P1+P2P_1 + P_2 avec une règle géométrique précise :

  1. Tracez la droite passant par P1P_1 et P2P_2.
  2. Elle coupe la courbe en un troisième point QQ.
  3. Le résultat P1+P2P_1 + P_2 est le symétrique de QQ par rapport à l'axe horizontal.

Diagramme conceptuel :

X Y P₁ P₂ Q P₁+P₂ ← la droite coupe la courbe en Q ; P₁+P₂ est le symétrique de Q

Quand P1=P2P_1 = P_2 (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 P1P2P_1 \neq P_2 : m=Y2Y1X2X1m = \dfrac{Y_2 - Y_1}{X_2 - X_1} Si P1=P2P_1 = P_2 : m=3X12+a2Y1m = \dfrac{3 X_1^2 + a}{2 Y_1}

Puis : X3=m2X1X2X_3 = m^2 - X_1 - X_2 Y3=m(X1X3)Y1Y_3 = m(X_1 - X_3) - Y_1

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, GG, comme point de départ. Ici, on prend simplement G=(1,2)G = (1, 2).

Tous les autres points seront générés par addition répétée : 2G=G+G2G = G + G, 3G=2G+G3G = 2G + G, 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 :

m=3X2+a2Y(mod101)m = \frac{3 X^2 + a}{2Y} \pmod{101}

Avec G=(1,2)G = (1, 2) et a=0a = 0 (notre courbe) :

m=3×122×2=34(mod101)m = \frac{3 \times 1^2}{2 \times 2} = \frac{3}{4} \pmod{101}

La « division par 4 » en arithmétique modulaire se fait en multipliant par l'inverse de 4. On a trouvé plus haut que 4176(mod101)4^{-1} \equiv 76 \pmod{101}.

m=3×76=2282282×101=26(mod101)m = 3 \times 76 = 228 \equiv 228 - 2\times101 = \mathbf{26} \pmod{101}

Puis on applique les formules des coordonnées :

X3=m2X1X2=26211=6762=6746746×101=68(mod101)X_3 = m^2 - X_1 - X_2 = 26^2 - 1 - 1 = 676 - 2 = 674 \equiv 674 - 6\times101 = \mathbf{68} \pmod{101}

Y3=m(X1X3)Y1=26×(168)2=26×(67)2Y_3 = m(X_1 - X_3) - Y_1 = 26 \times (1 - 68) - 2 = 26 \times (-67) - 2

6734(mod101)-67 \equiv 34 \pmod{101}, donc 26×34=88426 \times 34 = 884, et 8842=8828828×101=74(mod101)884 - 2 = 882 \equiv 882 - 8\times101 = \mathbf{74} \pmod{101}.

2G=(68, 74)\boxed{2G = (68,\ 74)} \checkmark


Calcul de 3G = 2G + G

On additionne maintenant deux points différents (68,74)(68, 74) et (1,2)(1, 2). On utilise la pente ordinaire :

m=Y2Y1X2X1=274168=7267(mod101)m = \frac{Y_2 - Y_1}{X_2 - X_1} = \frac{2 - 74}{1 - 68} = \frac{-72}{-67} \pmod{101}

7229-72 \equiv 29 et 6734(mod101)-67 \equiv 34 \pmod{101}. L'inverse de 34 ? Puisque 34×3=1021(mod101)34 \times 3 = 102 \equiv 1 \pmod{101}, on a 341334^{-1} \equiv 3.

m=29×3=87(mod101)m = 29 \times 3 = \mathbf{87} \pmod{101}

Coordonnées du résultat :

X3=872681=756969=7500750074×101=26(mod101)X_3 = 87^2 - 68 - 1 = 7569 - 69 = 7500 \equiv 7500 - 74\times101 = \mathbf{26} \pmod{101}

Y3=87×(6826)74=87×4274=365474=3580358035×101=45(mod101)Y_3 = 87 \times (68 - 26) - 74 = 87 \times 42 - 74 = 3654 - 74 = 3580 \equiv 3580 - 35\times101 = \mathbf{45} \pmod{101}

3G=(26, 45)\boxed{3G = (26,\ 45)} \checkmark


Calcul de 4G = 3G + G (on peut aussi faire 2G + 2G)

On additionne (26,45)(26, 45) et (1,2)(1, 2) :

m=245126=43255876(mod101)m = \frac{2 - 45}{1 - 26} = \frac{-43}{-25} \equiv \frac{58}{76} \pmod{101}

761(mod101)76^{-1} \pmod{101} : on cherche xx tel que 76x176x \equiv 1. On trouve 76×4=304=3×101+176 \times 4 = 304 = 3\times101 + 1, donc 761476^{-1} \equiv 4.

m=58×4=2322322×101=30(mod101)m = 58 \times 4 = 232 \equiv 232 - 2\times101 = \mathbf{30} \pmod{101}

X3=302261=90027=8738738×101=65(mod101)X_3 = 30^2 - 26 - 1 = 900 - 27 = 873 \equiv 873 - 8\times101 = \mathbf{65} \pmod{101}

Y3=30×(2665)45=30×(39)4530×6245=186045=1815181517×101=98Y_3 = 30 \times (26 - 65) - 45 = 30\times(-39) - 45 \equiv 30\times62 - 45 = 1860 - 45 = 1815 \equiv 1815 - 17\times101 = \mathbf{98}

4G=(65, 98)\boxed{4G = (65,\ 98)} \checkmark


À partir de là, on laisse Python continuer. Le tableau complet des premiers points :

nnG
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é GG et un entier nn, calculer nGnG.
  • Très difficile : étant donné GG et un point QQ, retrouver nn tel que Q=nGQ = nG.

C'est comme mélanger des cartes : facile à faire, impossible à inverser.

Donc, si Harry envoie le point 5G=(12,32)5G = (12, 32) à 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 :

aG+bG=(a+b)Ga \cdot G + b \cdot G = (a + b) \cdot G

Autrement dit, on peut additionner des chiffres masqués sans les démasquer.

Exemple : Harry veut prouver à Griphook que 2+3=52 + 3 = 5 sans révéler ni 2 ni 3.

  • Il envoie 2G2G et 3G3G.
  • Griphook les additionne sur la courbe : il obtient un point QQ.
  • Griphook compare QQ à 5G5G (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 :

A(t)B(t)C(t)=H(t)Z(t)A(t) \cdot B(t) - C(t) = H(t) \cdot Z(t)

D'après Schwartz-Zippel, il suffit de la vérifier en un seul point aléatoire τ\tau. Mais :

  • Si Harry choisit τ\tau lui-même, il peut tricher (fabriquer un faux A(t)A(t) qui marche en τ\tau).
  • Si Griphook choisit τ\tau, Harry doit lui révéler ses polynômes (donc son secret).

Solution : un tiers de confiance (« trusted setup ») choisit τ\tau 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 :

  1. Choisit un τ\tau aléatoire dans Fp\mathbb{F}_p. (Pour notre exemple, τ=7\tau = 7.)
  2. Calcule les puissances de τ\tau « cachées » sur la courbe :

PK=[G, τG, τ2G, τ3G, τ4G]\text{PK} = [G,\ \tau G,\ \tau^2 G,\ \tau^3 G,\ \tau^4 G]

(Une puissance pour chaque degré possible de A,B,C,HA, B, C, H.)

  1. Calcule Z(τ)GZ(\tau) \cdot G comme clé de vérification pour Griphook.

  2. Détruit τ\tau et toute trace mémorielle (la mémoire de la Pensine est vidée).

Si jamais τ\tau 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 A(t)=43+95t+89t2+79t3A(t) = 43 + 95t + 89t^2 + 79t^3. Il ne connaît pas τ\tau, mais il a la PK.

Grâce à l'homomorphisme :

A(τ)G=(43+95τ+89τ2+79τ3)GA(\tau) \cdot G = (43 + 95\tau + 89\tau^2 + 79\tau^3) \cdot G =43G+95(τG)+89(τ2G)+79(τ3G)= 43 \cdot G + 95 \cdot (\tau G) + 89 \cdot (\tau^2 G) + 79 \cdot (\tau^3 G)

Chaque terme à droite peut être calculé avec la PK ! Harry obtient ainsi le point πA=A(τ)G\pi_A = A(\tau) \cdot G sans jamais connaître τ\tau ni A(τ)A(\tau).


Chapitre 12 : Les Couplages (Pairings) — la dernière pièce

12.1 Le problème restant

Griphook a reçu :

  • πA=A(τ)G\pi_A = A(\tau) \cdot G
  • πB=B(τ)G\pi_B = B(\tau) \cdot G
  • πC=C(τ)G\pi_C = C(\tau) \cdot G
  • πH=H(τ)G\pi_H = H(\tau) \cdot G
  • (Et la VK : Z(τ)GZ(\tau) \cdot G)

Il doit vérifier que :

A(τ)B(τ)=H(τ)Z(τ)+C(τ)A(\tau) \cdot B(\tau) = H(\tau) \cdot Z(\tau) + C(\tau)

L'addition de points marche (homomorphisme). Mais la multiplication de points ne marche pas naturellement : πA×πB\pi_A \times \pi_B ne veut rien dire sur la courbe.

12.2 La solution magique : le pairing

Un couplage bilinéaire est une fonction e(P,Q)e(P, Q) qui prend deux points et renvoie un nombre dans un autre groupe (un « ciel mathématique » différent). Sa propriété fondamentale :

e(aG, bG)=e(G,G)abe(a \cdot G,\ b \cdot G) = e(G, G)^{a \cdot b}

Autrement dit, le pairing transforme une multiplication cachée en une exponentiation.

Du coup, Griphook peut comparer :

e(πA,πB)=?e(πC+πHquelque chose lieˊ aˋ Z,)e(\pi_A, \pi_B) \stackrel{?}{=} e(\pi_C + \pi_H \cdot \text{quelque chose lié à }Z, \ldots)

Plus précisément, l'équation devient :

e(πA,πB)=e(πH,Z(τ)G)e(πC,G)e(\pi_A, \pi_B) = e(\pi_H, Z(\tau)\cdot G) \cdot e(\pi_C, G)

Si l'égalité tient, c'est que A(τ)B(τ)=H(τ)Z(τ)+C(τ)A(\tau) B(\tau) = H(\tau) Z(\tau) + C(\tau), 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 Fp12\mathbb{F}_{p^{12}}. 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 x3+x+5=35x^3 + x + 5 = 35 et du secret x=3x = 3, 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 ?

  1. Les 4 portes du circuit sont satisfaites par le witness honnête : on a bien 33=93·3 = 9, 93=279·3 = 27, etc.

  2. Le polynôme P(t)=A(t)B(t)C(t)P(t) = A(t)B(t) - C(t) 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.

  3. H(t)=30+90t+19t2H(t) = 30 + 90t + 19t^2 est calculé sans reste. C'est ce qui prouve l'honnêteté de Harry : la division de P(t)P(t) par Z(t)Z(t) tombe juste.

  4. Au point secret τ=7\tau = 7, on a A(τ)B(τ)=19A(\tau) \cdot B(\tau) = 19 et H(τ)Z(τ)+C(τ)=19H(\tau) \cdot Z(\tau) + C(\tau) = 19. L'égalité tient.

  5. Si Harry triche (en mettant x=4x = 4), la division laisse un reste non nul, et l'égalité finale ne tiendrait plus.


Chapitre 15 : Récapitulatif Visuel

ÉQUATION DE HARRY x³ + x + 5 = 35 secret : x = 3 Flattening 4 PORTES + WITNESS x·x=v₁, v₁·x=v₂, v₂+x=v₃, v₃+5=out W = [1, 3, 35, 9, 27, 30] Sélecteurs vectoriels R1CS : 3 MATRICES A, B, C chaque porte vérifie A·W × B·W = C·W Interpolation de Lagrange QAP : 3 POLYNÔMES A(t), B(t), C(t) encodent toutes les portes en même temps Égalité maîtresse A(t)·B(t) − C(t) = H(t)·Z(t) Harry doit fournir H(t) sans reste Évaluation au point secret τ · courbe elliptique PREUVE : π_A, π_B, π_C, π_H 4 points géométriques — le secret est invisible Vérification par couplages e(π_A, π_B) = e(π_H, Z(τ)G) · e(π_C, G) ? → OUI : coffre ouvert NON : imposteur

É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 10210^{-2} — 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èmeType de preuveModèle cibleOverheadInnovation principaleRéférence
zkLLM (2024)Interactive (sumcheck)LLaMA-2-13B~15 min/passetlookup, zkAttnarXiv:2404.16109
zkGPT (2025)Non-interactive (SNARK)GPT-2<25 secConstraint fusion, circuit squeezeePrint:2025/1184
TOPLOC (2025)LSH (heuristique)LLaMA 3.1-8B258 B / 32 tokensEmpreinte polynomiale des activationsarXiv:2501.16007
VeriLLM (2026)Re-run léger + on-chainGénéral~1% du coûtArchitecture isomorphe, incitationsarXiv:2509.24257
ZK-DeepSeek (2025)SNARK non-interactifDeepSeekPreuve taille fixeFiat-Shamir sur transformerarXiv:2511.19902
ZKPROV (2025)ZKP sur datasetsGénéralProvenance des données d'entraînementarXiv: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é.