ZK와 선택적 공개
도입
ZK는 데이터를 숨기면서도 어떤 주장이 참이라는 증거를 제출하는 방법이다. Stablecoin 제품에서 중요한 질문은 "모든 것을 숨길 수 있는가"가 아니라 "어떤 정보는 숨기고, 어떤 정보는 감사 가능하게 남길 것인가"다.
선택적 공개는 privacy와 compliance 사이의 현실적인 중간 지점이다. 사용자는 나이, 지역, KYC 통과 여부, 거래 한도 준수 여부를 모두 공개하지 않고도 필요한 조건만 증명할 수 있다. 하지만 proof가 어떤 데이터와 어떤 시점에 대한 것인지 명확하지 않으면 compliance 증거로 쓸 수 없다.
학습 목표
- ZK가 무엇을 숨기고 무엇을 증명하는지 설명한다.
- 선택적 공개를 결제 compliance 요구와 연결한다.
개념 설명
ZK와 선택적 공개를 제품 설계에 넣어도 되는가?
숨길 데이터와 공개할 데이터가 분리되는가
체인별 ownership 모델을 오해하지 않는가
감사 가능성이 유지되는가
ZK 설계는 public input과 private witness를 나누는 것에서 시작한다.
| 사용 사례 | public input | private witness | 주의할 점 |
|---|---|---|---|
| KYC selective disclosure | verifier, issuer, credential type, expiry | 생년월일, 국가, KYC record | issuer 신뢰와 revocation 확인 |
| proof of reserve | totalSupply, snapshot id, reserve threshold | reserve account detail, custodian data | source data가 거짓이면 proof도 의미가 약함 |
| transaction limit | limit, token, period | 실제 누적 지출 | stale spending snapshot 방지 |
| sanction screening | list version hash, issuer | identity matching result | false positive/negative 운영 절차 |
ZK는 statement를 증명한다. statement가 모호하면 proof도 모호하다.
Bad statement: "사용자는 안전하다."Better statement: "issuer X가 2026-05-15 00:00 UTC 기준으로 발급한 credential에 따르면, 이 wallet controller는 allowlisted jurisdiction에 있고 sanctions list version Y에 매칭되지 않는다."선택적 공개 proof는 한 번 만들어 제출하는 파일이 아니라 lifecycle을 가진다. issuer가 credential을 발급하고, 사용자가 proof를 만들고, verifier가 검증하며, revocation이나 expiry가 생기면 같은 proof를 더 이상 받아들이지 않아야 한다.
이 lifecycle에서 disclosure log는 민감 데이터를 저장하는 곳이 아니다. verifier, issuer, statement id, proof generatedAt, list version, outcome처럼 나중에 감사자가 판단할 수 있는 최소 메타데이터를 남기는 곳이다.
코드로 확인하기
선택적 공개는 "개인정보를 숨긴다"가 아니라 "검증에 필요한 주장만 공개한다"는 방식이다. 코드는 private input, public signal, verifier 호출을 분리해야 한다.
클라이언트공개 신호를 최소화한 증명 요청
type KycProofRequest = { credentialHash: string; privateCountryCode: string; privateBirthDate: string; publicAllowedRegion: string; publicMinimumAge: number;};function buildPublicSignals(request: KycProofRequest) { return { allowedRegion: request.publicAllowedRegion, minimumAge: request.publicMinimumAge, credentialCommitment: request.credentialHash };}여기서 country와 birthDate는 private input이다. verifier와 merchant는 "허용 지역이며 최소 나이를 넘었다"는 public signal만 받는다.
컨트랙트verifier 결과로 결제 가능 상태만 갱신
function submitKycProof( bytes calldata proof, uint256[3] calldata publicSignals) external { bool ok = verifier.verifyProof(proof, publicSignals); if (!ok) revert InvalidProof(); bytes32 credentialCommitment = bytes32(publicSignals[2]); verifiedCredential[msg.sender] = credentialCommitment; emit CredentialVerified(msg.sender, credentialCommitment);}컨트랙트는 주민번호, 생년월일, 국가 코드를 저장하지 않는다. 결제 시스템에는 "이 주소가 정책을 통과했다"는 최소 증거만 남긴다.
강의 포인트
| 관점 | 확인할 질문 | 증거로 남길 것 |
|---|---|---|
| 공개 입력 | verifier가 검증해야 하는 최소 공개 값은 무엇인가 | public input table |
| 숨기는 데이터 | private witness에 남길 민감 데이터는 무엇인가 | witness boundary note |
| 시점 | proof가 어떤 snapshot과 expiry를 가리키는가 | freshness policy |
| circuit 운영 | circuit upgrade, trusted setup, verifier version을 어떻게 관리하는가 | circuit governance memo |
| 사용자 UX | proof 생성 실패와 latency를 어떻게 안내하는가 | proof UX state |
proof freshness는 별도 정책으로 둔다. KYC credential은 몇 달 동안 유효할 수 있지만 sanctions list와 reserve snapshot은 더 자주 바뀐다.
| 증명 유형 | freshness 기준 | 오래된 proof 처리 |
|---|---|---|
| KYC 통과 | credential expiry와 issuer revocation | 재발급 또는 re-check 요청 |
| 제재 미해당 | sanctions list version과 check timestamp | 최신 list 기준 proof 재생성 |
| 거래 한도 이하 | spending period와 ledger snapshot | 누적 지출 재계산 후 재증명 |
| reserve threshold | snapshot id와 custodian timestamp | stale reserve로 표시하고 결제 정책에 반영 금지 |
실무 예시
privacy stablecoin checkout에서 사용자가 "제재 대상이 아니고 결제 한도 이하"임을 증명한다고 가정한다. Merchant는 payer의 전체 신원과 잔고를 볼 필요가 없다. Merchant에게 필요한 것은 payment eligibility proof, amount received, settlement receipt다.
| Actor | 알아야 하는 것 | 알 필요 없는 것 |
|---|---|---|
| User | proof가 무엇을 증명하고 어디에 제출되는지 | 다른 사용자의 credential |
| Merchant | payment eligible, amount settled | payer의 전체 KYC record |
| Issuer | credential 발급과 revocation 상태 | merchant의 내부 재고/가격 전략 |
| Auditor | 필요한 시점의 disclosure record | 모든 사용자의 실시간 활동 |
실패 UX도 미리 정해야 한다.
| 실패 상태 | 사용자에게 보일 문구 | 운영자가 확인할 것 |
|---|---|---|
| ProofExpired | 증명 시간이 만료되어 다시 확인이 필요합니다 | generatedAt, expiry, verifier version |
| IssuerRevoked | 발급 기관 상태가 변경되어 재검증이 필요합니다 | issuer revocation registry |
| ListVersionStale | 최신 기준으로 다시 확인해야 합니다 | sanctions list version |
| VerificationFailed | 제출된 증명을 검증하지 못했습니다 | circuit id, public input, verifier log |
흔한 오해와 실패 시나리오
| 오해 | 실제로 확인할 것 |
|---|---|
| ZK를 쓰면 compliance 문제가 자동 해결된다고 본다. | statement, issuer trust, revocation, snapshot freshness가 있어야 한다. |
| proof가 오래돼도 검증만 되면 충분하다고 본다. | reserve, KYC, sanctions proof에는 expiry와 list version이 필요하다. |
| private witness를 숨기면 metadata leakage도 사라진다고 본다. | timing, counterparty, gas, network metadata는 별도 분석이 필요하다. |
| verifier gas cost를 나중에 보면 된다고 본다. | proof verification cost와 latency는 checkout UX에 직접 영향을 준다. |
| regulatory disclosure를 예외 처리로만 둔다. | selective disclosure policy와 audit log를 설계해야 한다. |
실습 과제
- ZK와 선택적 공개 이해 점검: KYC 통과, 제재 미해당, 거래 한도 이하, reserve threshold 증명에 대해 public input과 private witness를 분리한다.
- 결제 온보딩 흐름 설계: 사용자가 proof를 생성하고 merchant가 검증하며 실패 시 재시도/지원으로 넘어가는 상태를 만든다.
- freshness policy 작성: credential expiry, sanctions list version, reserve snapshot id, proof generatedAt을 어떻게 검증할지 정한다.
- tradeoff memo 작성: privacy, compliance, merchant UX, auditor access가 충돌하는 지점을 5개 적는다.
완료 기준
- ZK의 공개 입력을 설명했다.
- 선택적 공개 정책을 작성했다.
- compliance와 privacy tradeoff를 정리했다.
근거 자료
- 프라이버시 학습맵: 06-프라이버시-ZK-FHE/00-프라이버시-학습맵.md
- ZK 선택적공개 증명: 06-프라이버시-ZK-FHE/01-ZK-선택적공개-증명.md
- ZK FHE 프라이버시 문서: 90-출처/원문-노트/ZK-FHE-프라이버시-문서.md