Privacy primitives — ring signatures, nullifiers, and stealth addresses TODO
Concept
The common goal of privacy primitives is separating "proving you have authorization" from "revealing who you are." A ring signature proves that the signer holds the private key for one of a set of public keys, without revealing which one; the strength of the anonymity depends on the size and composition of that set (the anonymity set). A nullifier is a unique tag deterministically derived from a secret value; publishing it prevents double-spending while keeping it unlinkable to the specific deposit or commitment it came from. The same secret always yields the same nullifier, so a second attempt to use it is caught immediately. A stealth address is derived when the sender uses ECDH with the recipient's public meta-address to create a shared secret and derive a one-time receiving address — only the recipient can recognize and spend from that address, so their receiving history doesn't accumulate on-chain under a single address.
On a public ledger, a single address exposes an entire transaction history and balance, making it possible to read a counterparty's position and front-run them — that's a privacy problem that turns straight into financial loss. Understanding privacy design is what lets you decide which information really needs to stay on-chain.
Code & Formula
# 프라이버시 프리미티브 — nullifier(중복 사용 방지)와 스텔스 주소(toy DH),
# 그리고 링 서명(AOS, 1-of-n)으로 "권한 증명"과 "신원 노출"을 분리하는 예시.
# 그룹은 학습용 소수: p=23, q=11(부분군 order), g=2. 실무 곡선 크기가 아니라 예시용.
import hashlib, random
p, q, g = 23, 11, 2
def H(*args) -> int:
return int(hashlib.sha256("|".join(map(str, args)).encode()).hexdigest(), 16) % q
# --- 1) nullifier: 같은 비밀로는 항상 같은 태그가 나와 중복 사용을 검출 ---
spent = set()
def try_spend(secret):
tag = H("nullify", secret)
if tag in spent:
return False
spent.add(tag)
return True
secret = 424242
print("첫 인출:", try_spend(secret)) # True
print("같은 비밀로 재인출 시도:", try_spend(secret)) # False — nullifier 재사용 검출
# --- 2) 스텔스 주소: toy Diffie-Hellman 로 송/수신자가 독립적으로 같은 주소 유도 ---
recv_priv = random.randrange(1, q); recv_pub = pow(g, recv_priv, p)
eph_priv = random.randrange(1, q); eph_pub = pow(g, eph_priv, p)
addr_sender = H("addr", pow(recv_pub, eph_priv, p))
addr_receiver = H("addr", pow(eph_pub, recv_priv, p))
print("스텔스 주소 일치(송신자==수신자 유도):", addr_sender == addr_receiver)
# --- 3) 링 서명(AOS): n명 중 누가 서명했는지 숨긴 채 "그중 하나"임만 증명 ---
def ring_sign(msg, pubs, idx, x):
n = len(pubs); c = [0] * n; z = [0] * n
k = random.randrange(1, q)
i = (idx + 1) % n
c[i] = H(msg, pow(g, k, p))
while i != idx:
z[i] = random.randrange(1, q)
c[(i + 1) % n] = H(msg, pow(g, z[i], p) * pow(pubs[i], c[i], p) % p)
i = (i + 1) % n
z[idx] = (k - c[idx] * x) % q
return c[0], z
def ring_verify(msg, pubs, c0, z):
c = c0
for i in range(len(pubs)):
c = H(msg, pow(g, z[i], p) * pow(pubs[i], c, p) % p)
return c == c0
secrets = [random.randrange(1, q) for _ in range(3)]
pubs = [pow(g, x, p) for x in secrets]
c0, z = ring_sign("transfer 10", pubs, 1, secrets[1]) # 실제 서명자는 인덱스 1
print("링 서명 검증(서명자가 3명 중 누구인지는 드러나지 않음):", ring_verify("transfer 10", pubs, c0, z))
print("메시지 변조 시 검증 실패:", not ring_verify("transfer 99", pubs, c0, z))
docs/code/algorithms/algorithms-94.py
Exercise
Build a simple deposit/withdraw contract where deposits use a commitment hash and withdrawal is gated by publishing a nullifier so it can only happen once; test that a second withdrawal attempt with the same secret is blocked.
Practical Connection
In a prediction market, exposing the direction of a large position turns it straight into a front-running target, so Verex also needs to decide — from this angle — what order and settlement data to leave as public events versus what to keep off-chain.
Where it lands in Jayverse
- Verex: implement the nullifier scheme in the actual settlement contract. Beyond deciding what stays public, build the deposit/withdrawal gate itself — same secret always yields the same nullifier, so a second withdrawal attempt is blocked at the contract level, exactly as the exercise describes.
- Wallet: consider stealth addresses for the receive flow. So a user's incoming-payment history doesn't accumulate under one visible address, a separate privacy question from what Verex publishes as settlement events.
- Number: a ring-signature-style proof could gate licensed readings. A consumer proves they hold one of N valid licenses without revealing which subscriber they are, fitting the "reading as a licensed token" design.
Key expressions
| Expression | 뜻 · 쓰이는 자리 |
|---|---|
| separate X from Y | X와 Y를 구분해서 나누다 · "separating proving you have authorization from revealing who you are" |
| unlinkable to | ~와 연결 지을 수 없는 · "unlinkable to the specific deposit" |
| deterministically derived from | ~로부터 항상 똑같이 도출된 · "deterministically derived from a secret value" |
| accumulate under | (기록이) ~아래로 쌓이다 · "doesn't accumulate on-chain under a single address" |
| caught immediately | 즉시 적발되다 · "a second attempt to use it is caught immediately" |
| turn straight into | 곧바로 ~로 이어지다 · "turns straight into financial loss" |
| recognize and spend from | (자기 주소임을) 알아보고 그곳에서 인출하다 · "only the recipient can recognize and spend from that address" |
| ECDH | 타원곡선 디피-헬먼 키교환(Elliptic-Curve Diffie-Hellman) · 두 공개키로 공유 비밀을 계산해 스텔스 주소를 만드는 데 쓰임. "the sender uses ECDH with the recipient's public meta-address" |
| ring signature | 링 서명(ring signature) · 여러 공개키 집합 중 하나의 서명자임을 누구인지 감추고 증명하는 기법. "without revealing which one" |
| nullifier | 널리파이어(nullifier) · 비밀값에서 결정적으로 도출되는 고유 태그, 이중지불을 막되 원본과는 연결되지 않음. "publishing it prevents double-spending" |
| stealth address | 스텔스 주소(stealth address) · 수신자만 알아보고 인출할 수 있는 일회용 주소, 수신 이력이 한 주소에 쌓이지 않게 함. "derive a one-time receiving address" |
| anonymity set | 익명 집합(anonymity set) · 링 서명에서 서명자가 속할 수 있는 공개키 집합, 클수록 익명성이 강해짐. "the size and composition of that set" |
If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.