# Backend Issues

> "**Circuit은 내가 직접 짜지만, Proving System은 라이브러리를 믿고 쓴다."**

대부분의 ZK 애플리케이션 개발자는 이렇게 생각한다. 회로(Circuit) 레이어에서의 제약 조건 누락은 개발자의 실수로 받아들여지지만, 그 밑단에서 돌아가는 Groth16, PlonK, Halo2와 같은 증명 시스템(Backend)은 수많은 암호학자가 검증한 '절대 안전지대'라고 여기기 쉽다.

하지만 **백엔드야말로 ZK 시스템에서 가장 치명적이고 파괴적인 취약점이 숨어 있는 곳이다.** 회로의 결함은 해당 애플리케이션 하나를 망가뜨리지만, 백엔드의 결함은 그 시스템을 사용하는 모든 애플리케이션의 건전성(Soundness)을 근본부터 무너뜨리기 때문이다. 공격자가 비밀키 없이도 가짜 증명을 마음대로 찍어낼 수 있다면, 아무리 완벽하게 짜인 회로도 무용지물이 된다.

왜 이런 문제가 발생할까?&#x20;

이곳은 **추상적인 수학(Paper)이 구체적인 코드(Code)로 번역되는 지점**이기 때문이다. 논문에서는 "무작위 값을 선택한다"라고 한 줄로 적혀 있지만, 실제 코드에서는 해시 함수를 어떻게 구성하고, 어떤 변수를 포함해야 하며, 난수 생성기는 무엇을 써야 하는지 등 수많은 구현 디테일이 요구된다. 이 과정에서 발생한 아주 미세한 불일치가 'Frozen Heart'나 'Invalid Curve Attack' 같은 거대한 보안 구멍을 만들어낸다.

이번 글에서는 ZK 시스템의 엔진이라 할 수 있는 **백엔드 레이어(Backend Layer)**&#xC5D0;서 발생하는 주요 취약점들을 다룬다. Fiat-Shamir 변환의 구현 실수부터, 초기 설정(Trusted Setup)의 관리 소홀, 그리고 해시 함수와 타원곡선 같은 암호학적 원형(Primitive)을 잘못 다룰 때 어떤 재앙이 일어나는지 살펴본다. 이를 통해 우리는 라이브러리 내부를 맹신하는 것을 넘어, 시스템이 수학적으로 안전하기 위해 갖춰야 할 필수 조건들을 이해하게 될 것이다.

### 1. Frozen Heart : Fiat-Shamir 변환 취약점을 통한 증명 위조

**Frozen Heart**는 영지식 증명(ZKP) 프로토콜에서 **Fiat-Shamir 변환(Fiat-Shamir transformation)**&#xC744; 안전하지 않게 구현했을 때 발생하는 취약점 계열을 의미한다. 이 취약점이 존재하면 악의적인 증명자(Prover)가 검증 과정을 통과하는 **가짜 증명(Forged Proof)을 생성**할 수 있으며, 프로토콜에 따라 증명자가 원하는 어떤 내용이든 "증명"하는 것이 가능해질 수 있다.

대부분의 ZK 프로토콜은 본래 증명자와 검증자가 여러 번의 통신을 주고받는 '상호작용(Interactive)' 방식으로 설계된다. 이때 검증자는 증명자가 예측할 수 없는 무작위 값인 챌린지(Challenge)를 보내야 한다. 하지만 실용성을 위해 이를 '비상호작용(Non-interactive)' 방식으로 바꿀 때 Fiat-Shamir 변환을 사용한다. 정상적인 경우에는 증명의 모든 입력값(공개값, 커밋먼트 등)을 해시하여 챌린지를 생성한다. 그런데 Frozen Heart는 해시 생성 시 중요한 공개 입력값이나 커밋먼트를 누락하는 경우 발생한다. 이렇게 되면 챌린지가 증명자가 조작 가능한 특정 값에 의존하지 않게 되어, 공격자가 원하는 결과가 나올 때까지 값을 조작(Grinding)하거나 역산하여 가짜 증명을 만들어낼 수 있다.

**예시**

Frozen Heart 취약점의 핵심은 "**챌린지 생성 시 의존해야 할 변수가 누락됨"**&#xC774;다. 이를 이해하기 쉬운 의사 코드(Pseudo-code)로 표현하면 다음과 같다.

정상적인 Fiat-Shamir 구현은 다음과 같다. 챌린지(`challenge`)는 증명자가 생성한 `commitment`와 `public_input`에 종속된다. 따라서 증명자는 `commitment`를 먼저 확정해야만 `challenge`를 알 수 있다.

```python
# 증명자가 수행하는 로직
commitment = generate_random_commitment(secret)

# 모든 공개 정보를 해시하여 챌린지 생성
# 증명자가 commitment를 바꾸면 challenge도 바뀌므로 조작이 어려움
challenge = Hash(public_input, commitment) 

proof = calculate_proof(secret, challenge)
```

**그러나 Frozen Heart 취약점이 있는 코드는 다음과 같이** 개발자의 실수로 `commitment`가 해시 입력에서 누락되었을 때 발생한다.

```python
# 증명자는 commitment를 정하기 전에 challenge 값을 미리 알 수 있음
challenge = Hash(public_input) 

# 공격 시나리오:
# 1. challenge가 고정되어 있거나 미리 계산 가능함.
# 2. 공격자는 비밀 정보(secret)를 몰라도, 
#    이 고정된 challenge에 맞춰 검증을 통과하는 가짜 commitment와 proof를 역산해낼 수 있음.
fake_commitment, fake_proof = forge_proof(challenge)
```

이 경우 공격자는 유효한 `secret`을 몰라도 수학적 관계를 만족시키는 가짜 `commitment`와 `proof` 쌍을 찾아낼 자유도를 얻게 된다.

**실제 사례**

* Bulletproofs 논문 구현 오류

  Bulletproofs 프로토콜의 원본 논문에서 Fiat-Shamir 변환을 설명하는 방식에 결함이 있어 발생한 취약점이다. Bulletproofs는 타원곡선 점 $$g, h$$와 비밀값 $$v$$를 사용하는 페더슨 커밋먼트(Pedersen commitments, $$g^v h^\gamma$$)를 사용한다. 증명은 $$v$$가 특정 범위 내에 있음을 보여야 한다. 논문에서 제안한 Fiat-Shamir 구현 방식이 해시 계산 과정에서 **페더슨 커밋먼트 값을 누락**시켰다. 이로 인해 챌린지 값이 커밋먼트와 독립적으로 생성되었고, 공격자는 커밋먼트 값을 무작위로 계속 시도하여 $$v$$가 유효 범위를 벗어나더라도 검증을 통과하는 증명을 위조할 수 있었다.<br>
* PlonK 구현체의 Frozen Heart \
  PlonK 논문은 프로토콜을 비상호작용으로 만드는 방법은 설명했으나, 보안을 위해 해시에 포함되어야 할 입력값들에 대한 명확성이 부족했다. 다수의 PlonK 구현체들이 Fiat-Shamir 변환을 수행할 때 **모든 공개 입력값(Public Inputs)을 해시에 올바르게 포함하지 않았다**. 이로 인해 증명자에게 과도한 자유도가 주어졌고, 구현 세부 사항에 따라 정도의 차이는 있지만 악의적인 증명자가 증명을 위조할 수 있는 가능성이 열렸다.

### 2. Trusted Setup Leak : Toxic Waste 유출을 통한 증명 위조

**Trusted Setup Leak**는 ZK 프로토콜의 초기 설정 단계인 **신뢰 설정(Trusted Setup)** 과정에서 생성된 비밀 매개변수가 외부로 유출되는 취약점을 의미한다.

많은 ZK 프로토콜(특히 Pinocchio, Groth16, PLONK 등)은 증명자가 건전한(sound) 증명을 생성하기 위해 필요한 공개 매개변수(Prover Key, Verifier Key)를 생성하는 사전 설정 단계를 거친다. 이 과정에서 **'독성 폐기물(Toxic Waste)'**&#xC774;라 불리는 비밀 값(Trapdoor)이 생성되는데, 이 값은 설정이 끝난 후 반드시 영구적으로 삭제되어야 한다. 만약 이 독성 폐기물이 누군가에게 유출된다면, 해당 공격자는 실제 증거(Witness)를 몰라도 시스템의 검증을 통과하는 가짜 증명을 위조(Forge)할 수 있는 절대적인 권한을 갖게 된다.

**예시**

이 취약점의 핵심은 "**검증 우회를 가능케 하는 비밀 키(Trapdoor)의 노출"**&#xC774;다. 정상적인 경우에는 아래처럼 prover key와 verifier key를 생성한 후에 toxic waste가 삭제되어야 한다.

```python
# 1. 설정 단계 (Setup)
# prover_key와 verifier_key 생성 후 toxic_waste는 영구 삭제됨
(prover_key, verifier_key) = Setup(circuit, toxic_waste)
delete(toxic_waste) 

# 2. 증명 생성 (Prove)
# 증명자는 witness(비밀 입력값)가 있어야만 유효한 증명 생성 가능
valid_proof = Prove(prover_key, public_input, witness)
```

그런데 해당 취약점은 toxic waste가 삭제되지 않고, 공격자가 설정 과정의 결함이나 유출로 인해 `toxic_waste`를 얻은 경우에 발생한다.

```python
# 공격자가 toxic_waste를 획득함
captured_waste = toxic_waste

# 공격자는 witness를 몰라도(None), toxic_waste를 이용해 
# 수학적으로 검증을 통과하는 가짜 증명을 위조할 수 있음
fake_proof = Forge(captured_waste, public_input, fake_statement)

# 검증자는 이 증명을 유효한 것으로 착각하고 승인함
Verify(verifier_key, public_input, fake_proof) == True
```

**실제 사례**

* Zcash의 Counterfeiting 취약점

  Zcash의 초기 버전인 Sprout는 \[BCTV14] 논문의 설계를 기반으로 구현되었다. 그러나 해당 논문의 매개변수 설정(Parameter Setup) 알고리즘에는 수학적 오류가 존재했다. 그 결과, 신뢰 설정(Trusted Setup) 과정에서 생성되지만 실제 증명 과정에서는 사용되지 않아야 할 '우회 요소(bypass elements)'가 함께 생성되었다.

  이 우회 요소들은 설정 단계에서 생성된 MPC 트랜스크립트(transcript) 에 그대로 남았으며, 공격자는 이를 활용해 가짜 영지식 증명을 만들어낼 수 있었다. 이로 인해 공격자는 비밀키를 소유하지 않거나 실제 자산을 보유하지 않은 상태에서도 검증을 통과하는 유효한 증명을 생성할 수 있었으며, 결과적으로 가짜 코인을 발행하는 것이 가능했다.

### 3. **Bad Polynomial Implementation**

**Bad Polynomial Implementation**은 ZK 증명의 핵심인 다항식 연산(덧셈, 뺄셈, 곱셈 등)을 코드로 구현하는 과정에서, 수학적 정의와 실제 메모리 표현(Representation) 간의 불일치로 발생하는 취약점이다. 주로 연산 후 다항식의 차수(Degree)를 정리(Normalization)하지 않아 발생한다.&#x20;

예를 들어, 두 다항식을 더해서 최고차항의 계수가 0이 되어 차수가 낮아졌음에도 불구하고, 구현체가 이를 인지하지 못하고 여전히 높은 차수의 다항식으로 처리할 때 문제가 생긴다. 이는 계산 오류를 낳거나, 시스템이 예상치 못한 메모리 상태에 빠져 패닉(Panic)을 일으키고 서비스 거부(DoS) 상태가 되게 한다.

**예시**

다항식을 계수들의 배열(Vector)로 관리하는 시스템에서, 정규화(Normalization) 과정이 누락되었을 때 발생한다.

두 다항식 $$P\_1$$과 $$P\_2$$를 더하는 상황을 가정한다.

* $$P\_1 = 3 + 2x + x^2$$  → 벡터 표현: `[3, 2, 1]`
* $$P\_2 = 1 + (p-1)x^2$$  → 벡터 표현: `[1, 0, p-1]` (여기서 $$p-1$$은 유한체에서 $$-1$$과 동일하게 취급됨)

```rust
// 취약한 add 함수: 각 자리만 더하고 길이를 줄이지 않음
fn add(p1, p2) {
    // [3+1, 2+0, 1+(p-1)] = [4, 2, p] 
    // 유한체에서 p는 0이므로 결과는 [4, 2, 0]이 됨
    let result = vector_add(p1, p2); 
    return result; // 정규화(Trim) 없이 반환
}

// 차수 확인 함수: 마지막 요소가 0이 아니라고 가정함
fn degree(p) {
    // "다항식의 마지막 계수는 0일 수 없다"는 가정 하에 검증
    assert!(p.last() != 0); 
    return p.len() - 1;
}

// 실행 결과
let sum = add(p1, p2); // sum은 [4, 2, 0] (길이 3)
degree(sum); // 마지막 요소가 0이므로 assert 실패 -> 프로그램 Panic (Crash)
```

수학적으로 결과는 $$4+2x$$ (1차식)여야 하지만, 코드는 이를 처리하지 못하고 비정상 종료된다.

**실제 사례**

* Zendoo: 산술 연산 후 다항식 정규화 누락

  Zendoo 프로젝트의 `fft/polynomial/dense.rs` 파일은 다항식을 FFT(고속 푸리에 변환)에 사용하기 위해 밀집(Dense) 형태로 구현했다. 하지만 `add()` 함수 구현 중, 자신(`self`)의 차수가 상대방(`other`)보다 크거나 같을 때, 덧셈 연산 후 결과 벡터의 마지막에 남는 0(Trailing Zeros)을 제거하는 로직이 누락되어 있었다.

  ```rust
  // Zendoo의 취약한 add 함수 구현
  fn add(self, other: &'a DensePolynomial<F>) -> DensePolynomial<F> {
      if self.is_zero() {
          other.clone()
      } else if other.is_zero() {
          self.clone()
      } else {
          // [취약점 발생 지점]
          // self의 차수가 other보다 크거나 같은 경우
          if self.degree() >= other.degree() {
              let mut result = self.clone();
              for (a, b) in result.coeffs.iter_mut().zip(&other.coeffs) {
                  *a += b 
              }
              // 문제: 덧셈 결과 최고차항이 0이 되어도(상쇄되어도) 
              // result 벡터의 길이를 줄이는(pop) 코드가 없음.
              result 
          } else {
              // 반면, 이쪽 분기(else)는 정상적으로 처리하고 있음
              let mut result = other.clone();
              for (a, b) in result.coeffs.iter_mut().zip(&self.coeffs) {
                  *a += b
              }
              // 정상 로직: 최고차항 계수가 0이면 제거(pop)함
              while result.coeffs.last().unwrap().is_zero() {
                  result.coeffs.pop();
              }
              result
          }
      }
  }
  ```

  이후 시스템이 다항식의 차수를 확인하기 위해 `degree()` 함수를 호출할 때 문제가 터진다. 이 함수는 "다항식의 마지막 계수는 0일 수 없다"는 가정하에 `assert!`를 사용하고 있다. 위 `add` 함수에서 정규화되지 않은 다항식(예: `[4, 2, 0]`)이 생성되어 이곳으로 전달되면, `assert!`가 실패하여 프로그램이 패닉(Panic) 상태에 빠지고 노드가 멈추게 된다.

  ```rust
  /// 다항식의 차수를 반환하는 함수
  pub fn degree(&self) -> usize {
      if self.is_zero() {
          0
      } else {
          // 여기서 마지막 계수가 0인지 확인. 
          // 0이면 assert 실패 -> 프로그램 강제 종료 (Panic)
          assert!(self.coeffs.last().map_or(false, |coeff| !coeff.is_zero()));
          self.coeffs.len() - 1
      }
  }
  ```

### 4. Unsafe Cryptographic Primitive Implementation : 불안전한 암호학적 원형 구현

ZK 증명 시스템의 백엔드에서 해시 함수나 타원곡선과 같은 기본적인 암호학적 도구(Primitive)를 구현하거나 사용할 때, 필수적인 보안 속성을 간과하여 발생하는 취약점을 통칭한다. 암호학적 원형은 수학적으로 엄밀한 전제 조건(예: 해시의 충돌 저항성, 타원곡선 점의 유효성) 하에 안전성을 보장한다. 하지만 개발자가 구현 편의를 위해 **입력값의 구조(길이) 검증**을 누락하거나 **수학적 유효성(곡선 위 존재 여부) 검사**를 생략할 경우, 공격자는 이를 악용해 해시 충돌을 일으키거나 비밀키를 유출하는 등 치명적인 공격을 수행할 수 있다.

**예시**

이 취약점은 시스템이 입력값을 수학적으로 엄밀하게 검증하지 않고 신뢰할 때 발생한다. 크게 **해시 함수**와 **타원곡선** 두 가지 측면에서 나타난다.

1. 해시 함수 오용 (Hash Misuse)

   증명 시스템 내에서 사용되는 암호학적 해시 함수가 **제2 역상 저항성(Second Pre-image Resistance)**&#xC774;나 **길이 확장 공격(Length Extension Attack)**&#xACFC; 같은 보안 속성을 만족하지 못하게 구현되었을 때 발생한다. 일반적으로 ZK 시스템은 "암호학적으로 안전한 해시 함수(CHF)"를 사용한다고 가정하지만, 구현상의 실수로 인해 입력값의 길이를 검증하지 않거나, 내부 상태(Internal State)가 노출되는 구조를 사용할 경우 공격자가 해시 충돌을 유도하거나 조작된 데이터를 유효한 해시값으로 둔갑시킬 수 있다.

   * 시나리오 A: 제2 역상 공격 (Second Pre-image Attack)

     데이터의 구조나 길이를 해시 연산에 섞지 않아, 서로 다른 구조의 데이터가 같은 해시값을 갖게 된다. 배열 $$\[A, B, C]$$를 해시할 때 길이를 포함하지 않고 재귀적으로 $$H = h(h(A, B), C)$$와 같이 계산한다. 공격자는 $$S = h(A, B)$$를 미리 계산한 뒤, 새로운 배열 $$\[S, C]$$를 입력으로 제출한다. 시스템은 이를 원본 배열 $$\[A, B, C]$$와 구분하지 못한다.<br>
   * 시나리오 B: 길이 확장 공격 (Length Extension Attack)

     해시 함수 $$H(Secret, Data)$$의 출력값이 해시 함수의 내부 상태와 동일한 경우이다. 공격자는 $$H(Secret, Data)$$의 결과값만 알고 $$Secret$$은 모른다. 하지만 해시 알고리즘 특성상, 결과값(내부 상태)을 초기값으로 설정하고 추가 데이터(Append)를 넣어 연산을 이어서(Extension) 수행할 수 있다. 공격자는 $$Secret$$ 없이도 유효한 $$H(Secret, Data, Append)$$ 값을 계산해낼 수 있다.<br>
2. 타원곡선 검증 누락 (Curve Check Omission)

   타원곡선 암호(ECC)를 사용하는 프로토콜에서, 외부에서 입력받은 점 $$P(x, y)$$가 해당 시스템이 정의한 타원곡선 방정식(예: $$y^2 = x^3 + ax + b \pmod p$$)을 만족하는지, 그리고 올바른 필드 범위 내에 있는지 검증하지 않는 취약점이다. 타원곡선 연산은 입력된 점이 올바른 곡선 위에 있다는 수학적 전제하에 안전성을 보장한다. 만약 이 검증이 누락되면, 공격자는 위수(Order)가 작은 점 이나 곡선 밖의 점(Invalid Point)을 주입하여 시스템을 혼란에 빠뜨리거나 비밀키를 유출할 수 있다.

**실제 사례**

* Secure-starknet: 제2 역상 저항성 부재 (Second Pre-image)

  `hashChain` 함수는 Pedersen 해시를 기반으로 배열 값을 해싱하는 데 사용되었다. 하지만 이 함수는 해시 계산 과정에서 **데이터의 길이**나 시작 값(Seed)을 포함하지 않았다. 이로 인해 중첩된 배열 구조가 평면적인 배열 구조와 수학적으로 동일한 해시값을 갖게 되는 취약점이 발생했다.

  ```tsx
  import { hashChain } from "micro-starknet"

  // 1. 정상적인 배열 [1, 2, 3]의 해시값 계산
  // 내부적으로 h(h(1, 2), 3) 형태로 계산됨
  var h1 = hashChain([1, 2, 3])
  console.log(h1)

  // 2. 공격 준비: 앞부분 [2, 3]의 해시값을 미리 계산
  // (Note: 원본 예제 로직에 따라 순서가 다를 수 있으나 원리는 동일함)
  var h2 = hashChain([2, 3])
  console.log(h2)

  // 3. 공격 수행: [1, h2]라는 변조된 배열 입력
  // 내부적으로 h(1, h2) -> h(1, h([2, 3])) 형태로 계산되어 
  // 수학적으로 h1과 동일한 결과가 나옴
  var h3 = hashChain([1, h2])
  console.log(h3)

  // 서로 다른 입력이 같은 해시값을 가짐 (충돌 성공)
  // h1 === h3  ->  True
  ```
* Gnark: MiMC 해시의 길이 확장 공격 취약점

  Gnark 표준 라이브러리의 `MiMC` 해시 구현체는 **Miyaguchi-Preneel** 구조를 사용했다. 이 구조는 블록 암호를 해시로 변환하는 방식인데, 해시의 출력값(Digest)이 내부 상태(Internal State)와 동일하다는 특징이 있다. 만약 개발자가 이 해시 함수를 비밀값(Secret)과 함께 사용($$Hash(Key || Data)$$)한다면, 공격자는 비밀키를 몰라도 기존 해시값 뒤에 데이터를 덧붙여 새로운 유효 해시를 생성할 수 있다.

  ```tsx
  // Gnark 회로 정의 예시
  func (c *CircuitN) Define(api frontend.API) error {
      // MiMC 해시 인스턴스 생성
      h, err := mimc.New(api)
      if err != nil { return err }
      
      // [취약점 잠재 구간]
      // 비밀키(Key)를 먼저 쓰고 데이터를 씀 -> Hash(Key || Data)
      h.Write(c.Key)
      h.Write(c.Data[:]...)
      
      // 해시 결과 생성 (Sum)
      // Miyaguchi-Preneel 구조상, 이 'res' 값은 해시의 내부 상태와 같음.
      res := h.Sum()
      
      api.AssertIsEqual(res, c.Expected)
  }

  // 공격자는 'res' 값과 'Data'의 길이만 알면, 'Key'를 몰라도
  // h.Write(MoreData)를 추가로 수행하여 
  // Hash(Key || Data || MoreData)에 해당하는 유효한 증명을 위조할 수 있음.
  ```
* **Golang: `crypto/elliptic`의 잘못된 점 입력 허용**

  Go 언어의 표준 라이브러리인 `crypto/elliptic`은 타원곡선 연산을 구현한다. 그러나 과거 버전(Go 1.18 이전)의 API는 좌표값으로 음수(negative)나 필드 크기 $$P$$보다 큰 값(overflowing)이 들어오는 것을 막지 않았다. 또한, `IsOnCurve` 검사 없이 연산 함수를 호출할 경우, 엉뚱한 결과가 나오거나 내부 로직에서 패닉(Panic)이 발생할 위험이 있었다.

  ```go
  import (
      "crypto/elliptic"
      "math/big"
  )

  // 취약한 함수: 입력 점 (x, y)에 대한 검증 없이 연산 수행
  func VulnerableScalarMult(curve elliptic.Curve, x, y, scalar *big.Int) (*big.Int, *big.Int) {
      // 1. x, y가 필드 범위(0 ~ P-1) 내에 있는지 확인하지 않음 (음수, 오버플로우 허용)
      // 2. (x, y)가 곡선 방정식(y^2 = x^3 - 3x + b)을 만족하는지 확인하지 않음
      // 즉, 아래와 같은 방어 로직이 누락됨:
      // if !curve.IsOnCurve(x, y) { panic("Invalid Point") }

      // 라이브러리는 입력값이 유효하다고 가정하고 스칼라 곱셈을 수행함.
      // 공격자가 유효하지 않은 점을 입력하면, 이를 통해 비밀키(scalar)의 일부 정보가 유출될 수 있음.
      // (Invalid Curve Attack 가능성)
      resX, resY := curve.ScalarMult(x, y, scalar.Bytes())
      
      return resX, resY
  }
  ```

### 5. Unsafe Verifier Implementation : 불안전한 검증자 구현

검증자(Verifier)가 코드로 구현되는 과정에서 실수로 인해 '정당한 검증자'로서의 기능(유효하지 않은 증명을 거부하는 능력)을 상실하는 취약점이다. Verifier는 암호학적 연산(Pairing 등)을 수행한 뒤 그 결과가 '참'인지 확인하고, 만약 '거짓'이라면 반드시 실행을 멈추거나(Revert) 실패를 반환해야 한다. 하지만 개발자가 검증 결과값 확인을 누락하거나 입력값 검사를 생략하면, Verifier는 껍데기만 남게 되어 공격자가 제출한 가짜 증명(Fake Proof)이나 쓰레기 값을 그대로 통과시키게 된다.

**예시**

검증 함수가 명백히 실패(False)를 반환했음에도 불구하고, 이를 무시한 채 통과시키는 경우이다. 일반적으로 영지식 증명 검증의 최종 단계에서는 타원곡선 페어링(Pairing)과 같은 수학적 등식이 성립하는지 확인하게 된다.

정당한 검증자라면 이 과정에서 `PairingCheck`가 `False`를 반환할 경우 즉시 에러를 발생시켜 증명을 거절해야 한다. 그러나 취약하게 구현된 검증자는 함수가 `False`를 리턴했음에도 불구하고 별도의 조치 없이 '함수가 실행되었다'는 사실만으로 넘어가 버린다. 이 경우 검증자는 수학적 연산만 수행할 뿐 실제 판결은 내리지 않으므로, 사실상 검증자가 존재하지 않는 것과 다를 바 없다. 이를 의사 코드로 표현하면 다음과 같다.

```python
def verify_proof(proof):
    # pairing_check 함수는 증명이 틀리면 False를 반환함.
    # 하지만 개발자가 그 반환값을 변수에 담거나 if문으로 확인하지 않음.
    pairing_check(proof) 
    
    # 위에서 False가 났어도 프로그램은 계속 진행됨.
    return True # 무조건 통과
```

### Reference

* <https://github.com/timimm/awesome-zero-knowledge-proofs-security?tab=readme-ov-file>
* <https://github.com/0xPARC/zk-bug-tracker>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://upsidezkp.gitbook.io/upside-zkp-docs/step-5/zkp/backend-issues.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
