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.
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/.