Go 언어에서 gnark 라이브러리를 활용하여 zk-SNARKs circuit를 작성해보자

Sigrid Jin
26 min readMay 14, 2023

--

https://docs.gnark.consensys.net/

ZK 앱을 만들 때 일반적으로 Circom과 같은 DSL을 사용한다. Circom과 같은 DSL은 Snarks 서킷을 제작할 때 quadratic 변수 제한과 같은 사항과 최적화 이슈에 대해 크게 고민하지 않아도 쉽게 작성할 수 있도록 도와준다. 하지만 요구사항이 복잡해지는 경우에는 cryptographic library를 직접 import해서 서킷을 작성해야 하는 필요사항이 생긴다. 암호학 라이브러리가 가장 활성화된 언어는 Rust이다. Snarks 서킷을 작성할 때 Rust의 Arkworks를 사용하는 것이 best practice라고 알려져 있다.

하지만 Rust 언어는 시스템 프로그래밍에서 강력한 안정성을 제공하는 만큼 언어를 활용하는 것이 까다롭다고 알려져 있다. 따라서 언어의 장벽을 크게 고려하지 않고도 확장성 있는 snarks circuit 작성 라이브러리의 필요성이 대두되었다. Consensys는 Golang에서 SNARKs 서킷을 작성할 수 있는 라이브러리인 gnark를 프리뷰 버전으로 개발하고 있다. 본 글에서는 gnark에 대해 간단하게 알아보는 시간을 가져보겠다.

주요 Terminology 정리

ZK는 용어가 난해하므로 정리하고 넘어가보자. 참고 링크는 다음과 같다.

  • Field Element: 유한체 내의 숫자다. 필드 요소 연산은 대략 ~252비트 정수인 p를 모듈로 p — 1로 수행된다.
  • Public Input: Prover와 Verifier 모두에게 알려진 공개 입력 변수이다.
  • Relation: 어떤 public input과 private input의 조합이 유효한지를 지정한다.
  • Arithmetization: Relation을 arithemetic expression으로 나타낸 것이다. 보통 다항식을 제약하는 형태(polynomial constraints)로 나나탄다.
  • Circuit: Relation을 코드로 구현한 것을 회로라고 부른다.
  • Advice Value: 회로에서 사용하는 중간 값이다.
  • Witness: Advice Value와 Private Input의 조합을 witnesses라고 부른다.
  • SNARK: SNARK는 Prover가 유효한 관계에 조합된 증거 값을 알고 있음을 증명하는 proof을 만들 수 있게 하는 알고리즘이다. 그 길이가 회로 크기에 대해 다항 로그급수(poly-logarithmic)이다.
  • PLONK: PLONK은 SNARK 알고리즘의 한 종류인 ‘Oecumenical Non-Interactive Arguments of Knowledge에 대한 라그랑주 기반 순열’의 약자다.
  • Halo2: Halo2는 bulletproofs를 사용하는 PLONK 알고리즘의 구현체이다.

Recalling SNARKs [Link]

Groth16은 SNARKs 알고리즘 체계에서 회로를 arithmetization으로 표현하는 일반적인 구현체이다. Groth16에서 제약 조건은 (∑_i a_i x_i)(∑_i b_i y_i) = ∑_i c_i z_i의 형태를 취한다. 이 방정식에서 a, b, c는 상수이고, x, y, z는 증명자가 알고 있는 Private Input에 따라 달라지는 변수다. DSL이던 gnark이건 임의의 프로그래밍 언어로 작성된 Circuit을 constraint system으로 변환하는 과정을 Arithmetization이라고 부른다.

여기서 중요한 점은 변수, 입력, 상수 등 제약 조건의 모든 구성 요소는 소수 p로 특징지어지는 유한 필드인 Fp 내에 존재한다는 점이다. 회로의 제약 조건 수를 합리적으로 유지하려면 필드 Fp 내에서 작업해야 한다. 갈로이스 필드(Galouis Field)라고도 하는 유한체는 그 군에 속하는 원소 수가 유한하다. 이 경우 Fp는 0에서 p-1(포함)까지의 정수 집합으로, 여기서 ‘p’는 소수이다. 덧셈, 뺄셈, 곱셈, 나눗셈 연산은 modulo p만큼 수행되므로 이러한 연산의 결과는 항상 0에서 p-1까지의 범위에 속한다.

SNARK의 맥락에서는 이 필드의 요소를 변수(variables)라고 부른다. 예를 들어, F7(여기서 p는 7) 필드에서 작업하는 경우 작업할 수 있는 숫자는 0, 1, 2, 3, 4, 5, 6이다. 연산을 수행할 때 이 닫힌 집합을 둘러싼다. 예를 들어, F7에서 5+4는 2(9 X 7은 2이므로)이고 6*6은 1(36 X 7은 1)로 연산할 수 있다. 회로 내의 산술 연산이 제약 조건 시스템의 연산과 일치하도록 함으로써 현실적으로 계산과 검증을 가능하게 하는 일관된 시스템을 구축할 수 있다는 점에서 이러한 대수적 성질은 중요하다.

다른 필드의 변수(Fr에서 r ≠ p)에 대해 연산하면 어떻게 될까? 소수 p로 특질되는 필드에서 산술 모듈로 r을 모방(mimic)하기 위해 추가적인 대수 제약 조건이 필요하기 때문에 회로의 효율성이 떨어진다. 이러한 추가 제약 조건은 회로의 복잡성을 크게 증가시켜 작업하기 더 어렵게 만든다. 참고로, Groth16 알고리즘을 활용하는 Circuit의 제약 조건 수는 BN254 타원 곡선에서 2억 5천만개를 넘을 수 없다.

따라서 SNARK 회로는 큰 소수 p를 모듈로 사용하는 유한 필드 산술을 기반으로 구축되기 때문에, 실수를 다루는 여러 프로그래밍 구조가 자연스럽게 표현되지 않고 효율적으로 구현하기 어렵다.

  • 부동 소수점 숫자: SNARK는 “큰 숫자”를 다루지만 실수를 다루지 않는다. 부동 소수점 연산을 구현하려면 부동 소수점 숫자를 큰 숫자로 인코딩해야 해서 복잡성이 증가한다. gnark 라이브러리는 emulated 패키지를 활용하여 이러한 문제점을 일부 해결하고자 노력하고 있다.
  • 조건문: if/else와 같은 조건 분기는 가능하지만, 각 분기마다 회로를 복제해야 하므로 효율성이 떨어진다.
  • 메모리 관리: SNARK에서는 정적 회로만을 표현하므로 동적 메모리 할당과 포인터 관리가 어렵다.

따라서 일반 프로그래밍 언어에서 널리 사용되는 실수 처리, 동적 메모리 관리 및 복잡한 조건부 분기와 같은 로직은 유한 필드 표현 내에서 표현하는 데 발생하는 오버헤드로 인해 SNARK 회로에서 작성하기 쉽지 않다는 것은 꼭 알아야 할 포인트이다.

Writing Circuits using gnark library

gnark 라이브러리를 사용하여 회로를 작성할 때는 프론트엔드와 Circuit 인터페이스를 각각 작성해야 한다. 먼저 Circuit 인터페이스에는 circuit의 제약 조건을 검사하는 Define 메소드를 구현해야 한다. Circuit에는 public input과 private inputfrontend.Variable 형태로 선언되어 있어야 하고, gnark 라이브러리를 컴파일 시점에서 선언된 필드를 재귀적으로 파싱하여 frontend.constraintSystem을 빌드한다.

type MyCircuit struct {
C myComponent
Y frontend.Variable `gnark:",public"`
}

type myComponent struct {
X frontend.Variable
}

func (circuit *MyCircuit) Define(api frontend.API) error {
// ... see Circuit API section
}

위의 코드에서 ‘Y’ 는 Public Input이고, ‘X’ 는 기본적으로 Private Input이다. 표준 Go 패키지에서 사용하는 것처럼 구조체 태그를 사용해 입력 선언에 중요한 메타데이터를 추가할 수 있다. 예를 들어, ‘-’ 태그를 사용하면 변수가 여러 곳에서 참조될 때 한 번만 인스턴스화하려는 경우에 유용하다.

CIrcuit의 Define 메소드를 frontend.API 객체를 사용하여 제약 조건을 선언한다. 예를 들어, x³ + x + 5 = y라는 3차 방정식의 해를 알고 있음을 증명하려면 다음과 같이 Define 메소드를 구현할 수 있다.

func Define(api frontend.API) error {
x3 := api.Mul(circuit.X, circuit.X, circuit.X)
api.AssertIsEqual(circuit.Y, api.Add(x3, circuit.X, 5))
}

다른 DSL은 gadget이라고 해서 Circuit에서 다른 Circuit을 불러와서 마치 함수처럼 사용할 수 있는데, gnark 라이브러리에서는 일반 메소드처럼 구현해서 불러와 사용할 수 있으므로 gadget이라는 개념을 불필요하게 도입하지 않았다. 이렇게 Golang의 패키지를 직접 사용할 수 있는 가능성을 열어두었다는 것이 gnark가 유연한 Circuit API 설계에 유리하다는 방증이기도 하다.

예를 들어, MiMC Hash 라이브러리를 이용해서 값을 해싱하는 경우를 생각해보면 다음과 같다. 예시에서 보듯 gnark/std 라이브러리에서 바로 가져와서 사용하면 된다.

func (circuit *mimcCircuit) Define(api frontend.API) error {
// ...
hFunc, _ := mimc.NewMiMC(api.Curve())
computedHash := hFunc.Hash(cs, circuit.Data)
// ...
}

또한, 다른 ZK DSL 언어와 마찬가지로 gnark에서는 기존의 조건문(if, else)을 직접 사용할 수는 없고 Select 메소드를 사용해야 한다. Select 메소드는 조건부 연산자처럼 동작하며 boolean 조건에 따라 두 값 중 하나를 선택하는 내부 구현으로 동작한다.

// Select if 'b' is true, yields 'i1', else yields 'i2'
func (cs *ConstraintSystem) Select(b Variable, i1, i2 interface{}) Variable

Loop를 돌 때는 일반적인 golang 언어처럼 for loop를 사용하면 되지만, 내부적으로는 반복문을 순차적으로 처리하도록 내부 회로를 바꾸어 번역하게 된다. 참고로, 개별적인 실행 과정으로 처리하도록 회로를 구성하면 각 단계별로 생성되는 제약 조건의 개수를 관리할 수 있기 때문에, 성능이 향상된다. 각각의 실행 과정을 개별적으로 회로에 표현하면 증명 생성과 검증 과정에서 필요한 계산량이 줄어들게 된다 (unrolling)

반복문을 한 번에 처리하려고 하면 회로가 복잡해지고 제약 조건의 개수가 늘어나, 증명 생성과 검증에 소요되는 시간과 자원이 더 많이 필요하게 된다. 반면, 반복문을 풀어서 각각의 실행 과정을 명확하게 표현하면 제약 조건의 개수를 줄일 수 있고, 증명 생성과 검증 과정의 성능이 향상된다.

func (circuit *Circuit) Define(api frontend.API) error {
for i := 0; i < n; i++ {
circuit.X = api.Mul(circuit.X, circuit.X)
}
api.AssertIsEqual(circuit.X, circuit.Y)
return nil
}

경우에 따라 회로에서 계산을 정의하는 것이 복잡하거나 계산 비용이 많이 들 수 있다. 이럴 때 Hint를 사용하면 Hint 내부의 frontend.Variable 값과 비교하여 회로 외부에서 정수에 대한 계산이 수행되고 Hint 메소드가 Hint의 새 변수에 외부 계산 결과를 제공할 수 있다. 계산이 회로 외부에서 수행되므로 결과의 정확성은 보장되지 않는다

예를 들어 힌트 메소드가 소수 n의 인수분해를 계산해야 한다고 생각해보자.

p, q <- hint(n) st. p * q = n

소수 p와 q로 이루어진 인수분해이므로 회로는 p * q가 실제로 n과 같다 즉, n == p * q 라는 것을 보여야 한다. 그러나 힌트 함수의 구현이 잘못되거나 어떠한 이유에서 힌트 변수에 잘못된 값이 대입된 경우 해당 회로의 proof는 의미가 없다. 따라서 Hint 메소드를 사용할 때는 매우 주의하여야 한다.

회로에서 힌트 메소드를 사용하려면 Function 인터페이스에 따라 힌트 함수 hintFn을 정의해야 한다. 그런 다음 회로에서 frontend.API.NewHint(hintFn, vars…) 를 사용하여 힌트 메소드를 구현해야 한다. 여기서 vars는 힌트 함수에 적용될 변수이다. 이 메소드는 회로에서 블랙박스처럼 사용할 힌트 변수를 반환한다.

힌트는 회로 관점에서 본질적으로 블랙박스이므로 회로에서 정의된 힌트는 Proof를 구성할 때 제약 조건에 맞는지 확인되지 않는다. 증명 구성 중 특정 힌트 메소드를 사용하도록 허용하려면 사용자는 사용 가능한 메소드를 나타내는 backend.ProverOption을 제공해야 한다. 사용 가능한 힌트 메소드는backend.WithHints(hintFns…) 메소드를 이용하여 얻을 수 있으며, 여기서 hintFns는 해당 힌트 함수이다.

예를 들어, 정수 a를 비트로 분해하는 경우를 생각해보자. 정수의 이진 표현을 확인하고 해당 표현에서 비트를 추출할 수 있겠지만, gnark는 기본 비트 연산을 제공하지 않는다. 대신 hint 함수인 hint.IthBit를 사용하여 회로 외부에서 비트를 변수로 제공하고 사용자는 가중치가 적용된 합계를 통해 변수를 a에 대응시켜야 한다. 예시를 살펴보자.

  var b []frontend.Variable
var Σbi frontend.Variable
base := 1
for i := 0; i < nBits; i++ {
b[i] = cs.NewHint(hint.IthBit, a, i)
cs.AssertIsBoolean(b[i])
Σbi = api.Add(Σbi, api.Mul(b[i], base))
base = base << 1
}
cs.AssertIsEqual(Σbi, a)

Compiling, Debugging and Testing Circuits

다음과 같이 회로를 컴파일 할 수 있다. 회로를 컴파일한다는 것은 Circuit을 R1CS proving scheme 시스템이 활용할 수 있도록 다항식의 표현으로 Arimetization 과정을 수행한다는 의미이다. 이는 간단하게 Playground를 통하여 체험해볼 수도 있다.

var myCircuit Circuit
r1cs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &myCircuit)

증명을 생성할 때 다음과 같은 오류가 가장 일반적이라고 한다. 이 오류는 prover가 제공한 witnesses가 회로에서 최소한 하나의 제약 조건을 만족시키지 못했다는 것을 의미한다. 디버깅하려면 상세한 스택 추적을 위해 -tags=debug로 회로의 에러 스택을 확인할 수 있다.

constraint is not satisfied: [(.. * ..) != (.. * ..) + (.. * ..) + (.. * ..)]

proof를 생성하고 검증하는 과정은 gnark/backend의 다음 메소드를 사용하면 된다.

type Circuit struct {
X frontend.Variable
Y frontend.Variable `gnark:",public"`
}

assignment := &Circuit {
X: 3,
Y: 35,
}
witness, _ := frontend.NewWitness(assignment, ecc.BN254)
// use the witness directly in zk-SNARK backend APIs

groth16.Prove(cs, pk, witness)
// test file --> assert.ProverSucceeded(cs, &witness)

// 1. One time setup
pk, vk, err := groth16.Setup(cs)
publicData, _ := plonk.Setup(cs, ...)

// 2. Proof creation
proof, err := groth16.Prove(cs, pk, witness)
proof, err := plonk.Prove(r1cs, publicData, witness)

// 3. Proof verification
err := groth16.Verify(proof, vk, publicWitness)
err := plonk.Verify(proof, publicData, publicWitness)

다음과 같이 ecc.BN254 + Groth16 알고리즘 조합에 대하여 Solidity Verifier 컨트랙트를 생성할 수도 있다.

// 1. Compile (Groth16 + BN254)
cs, err := frontend.Compile(ecc.BN254, r1cs.NewBuilder, &myCircuit)

// 2. Setup
pk, vk, err := groth16.Setup(cs)

// 3. Write solidity smart contract into a file
err = vk.ExportSolidity(f)

다음과 같이 모든 타원 곡선과 알고리즘 (Groth16, Plonk…) 에 대해 테스트해볼 수도 있다. Geth 이더리움 클라이언트를 local 환경에서 띄워 테스트도 해볼 수 있다.

// assert object wrapping testing.T
assert := test.NewAssert(t)

// declare the circuit
var cubicCircuit Circuit

assert.ProverFailed(&cubicCircuit, &Circuit{
PreImage: 42,
Hash: 42,
})

assert.ProverSucceeded(&cubicCircuit, &Circuit{
PreImage: 35,
Hash: "16130099170765464552823636852555369511329944820189892919423002775646948828469",
}, test.WithCurves(ecc.BN254))

EdDSA 연산결과 검증 Circuit 작성해보기

이제 gnark 라이브러리를 사용하여 EdDSA 서명의 정확성을 확인하는 회로를 구현하는 과정을 살펴보자. 이는 ZK Rollup에서 Sequencer가 사용자로부터 서명된 트랜잭션을 일괄 처리하고, 상태를 업데이트한 후, 모든 트랜잭션의 유효성을 증명하는 Proof를 생성해야 하는 경우에 특히 유용하다.

먼저, EdDSA 서명 스키마에 대해 살펴보자. 우리의 구현에서는 EdDSA는 일반적인 타원 곡선인 ed1559를 사용할 수 없다. SNARKS 회로에서는 변수들은 F_r에서 존재하는데 이는 ed1559의 정의 필드와 다르다. 따라서 JubJub(BLS12_381) 및 Baby JubJub(BN254)이라는 곡선이 제안되어 F_r에서 변수가 정의될 수 있도록 하였다. 이러한 곡선들은 gnark-crypto 라이브러리에서 찾을 수 있다.

EdDSA를 사용하는 일반적인 작업 흐름은 다음과 같다.

  1. 사용자는 SNARK 회로 외부에서 메시지에 서명한다.
privateKey, publicKey := eddsa.New(..)
signature := privateKey.Sign(message)

2. SNARK 회로 내부에서 EdDSA 서명의 유효성을 검증한다.

assert(isValid(signature, message, publicKey))

EdDSA 서명의 유효성을 확인하려면 다음 변수(witnesses)가 필요하다.

  1. 서명자의 공개 키: 특수 곡선의 점으로 표현되는 tuple 형태이다 (x, y). 또한 공개 키는 또한 특수 곡선의 파라미터를 저장하여 어떤 곡선을 사용했는지도 알 수 있게 한다. 회로 예시는 다음과 같다.
  2. 서명: 서명이 완료된 메시지의 EdDSA 서명은 튜플 (R, S)로 표현된다, 여기서 R은 특수 곡선의 점 (x, y)이고, S는 스칼라 값이다. 스칼라 S는 특수 곡선에서 스칼라 곱셈을 수행하는 데 사용된다.
// CurveParams twisted edwards curve parameters ax^2 + y^2 = 1 + d*x^2*y^2
// Matches gnark-crypto curve specific params
type CurveParams struct {
A, D, Cofactor, Order *big.Int
Base [2]*big.Int // base point coordinates
}
package eddsa

import "github.com/consensys/gnark/std/algebra/twistededwards"

type PublicKey struct {
A twistededwards.Point
}
import "github.com/consensys/gnark/frontend"

// Signature stores a signature (to be used in gnark circuit)
// An EdDSA signature is a tuple (R,S) where R is a point on the twisted Edwards curve
// and S a scalar. Since the base field of the twisted Edwards is Fr, the number of points
// N on the Edwards is < r+1+2sqrt(r)+2 (since the curve has 2 points of multiplicity 2).
// The subgroup l used in eddsa is <1/2N, so the reduction
// mod l ensures S < r, therefore there is no risk of overflow.
type Signature struct {
R twistededwards.Point
S frontend.Variable
}

위와 같이 SignaturePublicKey struct를 정의하였으므로, 이제 EdDSA 검증 알고리즘의 비즈니스 로직을 작성할 수 있다. 먼저 Verify 메소드를 구현한다. 이 함수는 서명, 메시지, 공개 키를 필요로 한다. 또한 gnark API의 함수를 호출하기 위해 frontend.ConstraintSystem 객체도 필요하다.

import (
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/std/algebra/twistededwards"
"github.com/consensys/gnark/std/hash/mimc"
)

func Verify(curve twistededwards.Curve, sig Signature, msg frontend.Variable, pubKey PublicKey, hash hash.Hash) error {

// compute H(R, A, M)
data := []frontend.Variable{
sig.R.A.X,
sig.R.A.Y,
pubKey.A.X,
pubKey.A.Y,
msg,
}
hramConstant := hash.Hash(cs, data...)

return nil
}

다음으로, MiMC 해시 함수를 사용하여 H(R, A, M)을 계산하도록 Verify 메소드의 비즈니스 로직을 구현한다. 그런 다음 등식의 왼쪽 항 [2^c*S]G 를 계산하고, 오른쪽 항 [2^c]R + [2^cH(R,A,M)]A 를 계산하도록 선언한다.

// [2^basis*S1]G
lhs.ScalarMulFixedBase(cs, pubKey.Curve.BaseX, pubKey.Curve.BaseY, sig.S1, pubKey.Curve).
ScalarMulNonFixedBase(cs, &lhs, basis, pubKey.Curve)

// [S2]G
tmp := twistededwards.Point{}
tmp.ScalarMulFixedBase(cs, pubKey.Curve.BaseX, pubKey.Curve.BaseY, sig.S2, pubKey.Curve)

// [2^basis*S1 + S2]G
lhs.AddGeneric(cs, &lhs, &tmp, pubKey.Curve)

// [2^c*(2^basis*S1 + S2)]G
lhs.ScalarMulNonFixedBase(cs, &lhs, cofactorConstant, pubKey.Curve)

lhs.MustBeOnCurve(cs, pubKey.Curve)
//rhs = [2^c]R+[2^cH(R,A,M)]A
// M: message
// A: public key
// R: from the signature (R,S)
rhs := twistededwards.Point{}
rhs.ScalarMulNonFixedBase(cs, &pubKey.A, hramConstant, pubKey.Curve).
AddGeneric(cs, &rhs, &sig.R.A, pubKey.Curve).
ScalarMulNonFixedBase(cs, &rhs, cofactorConstant, pubKey.Curve)
rhs.MustBeOnCurve(cs, pubKey.Curve)

마지막으로, 이제 위에서 사용한 변수 cs의 타입인 *frontend.ConstraintSystem 에서 구현한 verify 메소드를 실제 입력값을 검증하는 과정에서 적용해보도록 하자. 이 때 gnark API의 AssertIsEqual 메소드를 사용하여 왼쪽 항과 오른쪽 항이 같음을 확인한다. 이를 위해 AssertIsEqual을 왼쪽 항과 오른쪽 항의 X 스칼라 값과 Y의 스칼라 값에 각각 연산해본다.

// ensures that lhs==rhs
api.AssertIsEqual(lhs.X, rhs.X)
api.AssertIsEqual(lhs.Y, rhs.Y)

예를 들어, Alice와 Bob이 거래를 하려고 한다. Alice는 Bob에게 10 BTC 보내려고 하며, 이 거래의 유효성을 증명하기 위해 EdDSA 서명을 생성한다. 이 서명은 Alice의 Private Key를 사용하여 생성되며, 이후에 Alice의 Public Key를 사용하여 확인할 수 있다. 이 과정에서, zk-Rollup 시퀀서는 Alice와 Bob 사이의 거래가 올바른지 확인하고, 전체 시스템의 상태를 업데이트하며, 모든 거래에 대한 SNARK proof를 생성한다.

아래 코드는 Circuit을 정의하고, EdDSA 라이브러리의 verify 메소드를 Circuit의 Define 메소드에서 호출하여 입력값을 검증하고, main 고루틴에서 EdDSA 메시지 서명을 만들어 이를 검증하고 Proof를 만들어 Verify까지 하는 과정을 보여주고 있다.

import (
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/std/signature/eddsa"
)

type eddsaCircuit struct {
PublicKey eddsa.PublicKey `gnark:",public"`
Signature eddsa.Signature `gnark:",public"`
Message frontend.Variable `gnark:",public"`
}
import (
"github.com/consensys/gnark/std/algebra/twistededwards"
"github.com/consensys/gnark-crypto/ecc"
)

func (circuit *eddsaCircuit) Define(api frontend.API) error {
curve, err := twistededwards.NewEdCurve(api, circuit.curveID)
if err != nil {
return err
}

mimc, err := mimc.NewMiMC(api)
if err != nil {
return err
}

// verify the signature in the cs
return eddsa.Verify(curve, circuit.Signature, circuit.Message, circuit.PublicKey, &mimc)

return nil
}
func main() {
// instantiate hash function
hFunc := hash.MIMC_BN254.New()

// create a eddsa key pair
privateKey, err := eddsa.New(twistededwards.BN254, crand.Reader)
publicKey := privateKey.Public()

// note that the message is on 4 bytes
msg := []byte{0xde, 0xad, 0xf0, 0x0d}

// sign the message
signature, err := privateKey.Sign(msg, hFunc)

// verifies signature
isValid, err := publicKey.Verify(signature, msg, hFunc)
if !isValid {
fmt.Println("1. invalid signature")
} else {
fmt.Println("1. valid signature")
}
}
var circuit eddsaCircuit
r1cs, err := frontend.Compile(ecc.BN254, r1cs.NewBuilder, &circuit)
// declare the witness
var assignment eddsaCircuit

// assign message value
assignment.Message = msg

// public key bytes
_publicKey := publicKey.Bytes()

// assign public key values
assignment.PublicKey.Assign(ecc.BN254, _publicKey[:32])

// assign signature values
assignment.Signature.Assign(ecc.BN254, signature)
// witness
witness, err := frontend.NewWitness(&assignment, ecc.BN254)
publicWitness, err := witness.Public()

// generate the proof
proof, err := groth16.Prove(r1cs, pk, witness)

// verify the proof
err = groth16.Verify(proof, vk, publicWitness)
if err != nil {
// invalid proof
}

유닛 테스트는 다음과 같이 작성해볼 수도 있다.

assert := groth16.NewAssert(t)
var witness Circuit
assert.ProverFailed(&circuit, &assignment) // .ProverSucceeded

--

--

No responses yet