
The Alberta Buck identity system (Identity, Examples) rests on
two cryptographic transformations: the Identity Fountain rerandomizes
a Pointcheval-Sanders signature to produce an unlinkable credential,
and the approve handshake re-encrypts the credential's ElGamal
ciphertext under the counterparty's public key with a Chaum-Pedersen
proof of equality. Everything downstream – bilateral transfers, the
BUCK Notes spend circuits (Notes), Identity Guardian authorizations,
regulatory recovery – inherits its correctness from these two
primitives.
This document states and proves the correctness properties that the rest of the series relies on:
- Theorem 1: rerandomization preserves PS signature validity (completeness).
- Theorem 2: a uniform random rerandomization scalar yields a signature statistically identical to a fresh one, independent of the original (unlinkability).
- Theorem 3: an honest Chaum-Pedersen prover always verifies (completeness).
- Theorem 4: from two accepting transcripts with distinct challenges an efficient extractor recovers the witness (special soundness, 2-extractability).
- Theorem 5: the sigma-protocol transcript is perfectly simulatable given only public inputs and the challenge (HVZK).
- Theorem 6: combined, an accepted on-chain Chaum-Pedersen proof implies that the re-encrypted ciphertext carries the same issuer-certified identity point \( M = m \cdot G \) as the registered credential (end-to-end re-encryption correctness).
- Theorem 7: ElGamal re-randomization at note mint preserves the recipient's encrypted identity point under their registered key (BUCK Notes mint correctness).
- Theorem 8: an accepted A-spend SNARK on a noteTree commitment implies, via Chaum-Pedersen equality, that the depositor's registered credential and the note's ciphertext encrypt the same identity point under the same public key – the \( pk_{\text{rec}} \) that was live at mint time. A-notes are bound to that key pair: if \( sk_{\text{rec}} \) is lost, Identity re-issuance to a fresh keypair \( (sk', pk') \) on the same \( M \) does not restore spend capability.
- Theorem 9: an accepted B-spend SNARK certifies (i) a registered depositor knowing the bearer secret \( \rho \) and (ii) a Schnorr-signed public-issuer attestation on the commitment.
- Theorem 10: the A-spend nullifier (keyed on \( M_{\text{rec}} \)) is a deterministic function of \( (M_{\text{rec}}, \rho) \) alone and is not precomputable by the issuer; the B-spend nullifier (keyed on \( \rho \)) is publicly derivable – the bearer-cheque property. Domain separation prevents cross-flavor collision.
- Theorem 11: Notes preserve the BUCK mutual-decryptability invariant – both counterparties recover the other's identity point in every accepted spend.
Every theorem is accompanied by an executable py_ecc sanity check on
BN254. The proofs assume only the q-SDH assumption (for PS signature
unforgeability), the discrete logarithm assumption on
\( \text{alt\_bn128}\ G_1 \), the random oracle model (for the
Fiat-Shamir transform and Poseidon hashing), and knowledge soundness
of the underlying SNARK proof system (for Theorems 8-9) – all
standard assumptions that Ethereum's security or any zk-SNARK rollup
already requires. (PDF, Text)
Preliminaries
Notation
| Symbol | Meaning |
|---|---|
| \( q \) | Prime order of \( G_1, G_2, G_T \) on BN254 |
| \( G, g_2 \) | Generators of \( G_1 \) and \( G_2 \) |
| \( e : G_1 \times G_2 \to G_T \) | Bilinear, non-degenerate pairing (type-3) |
| \( O \) | Point at infinity (group identity) |
| \( \mathbb{Z}_q^\ast \) | Non-zero scalars mod \( q \) |
| \( (x, y) / (X, Y) \) | Issuer secret / public key: \( X = x g_2,\ Y = y g_2 \) |
| \( m \in \mathbb{Z}_q \) | Identity scalar, \( m = H(\text{identity\_data}) \bmod q \) |
| \( M = m \cdot G \in G_1 \) | Identity point (the value every ElGamal ciphertext encrypts) |
| \( \sigma = (\sigma_1, \sigma_2) \in G_1^2 \) | PS signature: \( \sigma_1 = h,\ \sigma_2 = (x + m y) h \) |
| \( (sk, pk) \) | Identity key pair on \( G_1 \): \( pk = sk \cdot G \) |
| \( E = (R, C) \in G_1^2 \) | ElGamal ciphertext: \( R = r G,\ C = M + r \cdot pk \) |
| \( H(\cdot) \) | Cryptographic hash modelled as a random oracle (Fiat-Shamir) |
All group operations in this document are written additively. BN254 has cofactor 1 on \( G_1 \), so every non-identity \( P \in G_1 \) has order \( q \); there are no small-subgroup elements.
Hardness Assumptions
Four standard assumptions underpin the theorems in this document.
None is specific to Alberta Buck – each one is already required by
Ethereum's own security model or by any pairing-based / zk-SNARK
construction built on the ecPairing precompile.
- Discrete Logarithm (DLog) on \( G_1 \): given \( P, Q \in G_1 \), finding \( \alpha \in \mathbb{Z}_q \) with \( Q = \alpha P \) is infeasible. Protects ElGamal IND-CPA security, Schnorr unforgeability, and Chaum-Pedersen soundness.
- q-Strong Diffie-Hellman (q-SDH): the PS signature scheme is existentially unforgeable under chosen-message attack (EUF-CMA) assuming q-SDH1. We use this only as a stated assumption about the PS primitive; none of our proofs below reduce to it directly.
- Random Oracle Model (ROM): the Fiat-Shamir transform is applied to sigma protocols whose security is proved in the ROM, with the hash function \( H \) modelled as a random oracle bound to all public inputs and context (caller address, chain id, etc.). Poseidon (commitment hash and nullifier PRF) is also modelled as a random oracle for the BUCK Notes proofs in Part IV.
- Knowledge soundness of the underlying zk-SNARK proof system (Part IV only): the SNARK system used for BUCK Notes spend circuits (Groth16, PLONK, or any knowledge-sound construction over BN254) admits a polynomial-time witness extractor on every accepting proof. This is a system-specific assumption depending on the chosen scheme's trusted setup or universal setup.
The rerandomization proofs (Part I) are unconditional: Theorem 1 follows from bilinearity alone, and Theorem 2 is information-theoretic. The Chaum-Pedersen proofs (Part II) are interactive-sigma-protocol facts; the NIZK corollary (Part II, end) invokes the ROM. Part IV (Notes spend predicates) additionally invokes assumption (4) – knowledge soundness of the underlying SNARK system – to lift the sigma-protocol guarantees of Theorems 4-5 into the SNARK setting.
Part I: Pointcheval-Sanders Signature Rerandomization
The Construction
A Pointcheval-Sanders signature on \( m \in \mathbb{Z}_q \) is a pair \( \sigma = (\sigma_1, \sigma_2) \in G_1^2 \) with \( \sigma_1 \neq O \) satisfying the pairing equation
\[ e(\sigma_1,\; X + m \cdot Y) \;=\; e(\sigma_2,\; g_2). \]
The issuer produces \( \sigma \) by choosing a fresh \( h \in G_1 \setminus \{O\} \) and setting \( \sigma_1 = h,\ \sigma_2 = (x + m y) h \). Rerandomization is the operation
\[ \sigma' \;=\; (t \cdot \sigma_1,\; t \cdot \sigma_2) \qquad t \in \mathbb{Z}_q^\ast. \]
Theorem 1 (Completeness of Rerandomization)
Statement. For every valid PS signature \( \sigma \) on \( m \) under issuer key \( (X, Y) \), and for every \( t \in \mathbb{Z}_q^\ast \), the pair \( \sigma' = (t \sigma_1, t \sigma_2) \) is also a valid PS signature on \( m \) under the same issuer key.
Proof. By bilinearity of \( e \):
\begin{align*} e(\sigma'_1,\; X + m Y) &= e(t \sigma_1,\; X + m Y) & \text{(definition)} \\ &= e(\sigma_1,\; X + m Y)^t & \text{(bilinearity)} \\ &= e(\sigma_2,\; g_2)^t & \text{(original validity)} \\ &= e(t \sigma_2,\; g_2) & \text{(bilinearity)} \\ &= e(\sigma'_2,\; g_2). \end{align*}Non-degeneracy: \( G_1 \) has prime order \( q \), so \( t \sigma_1 = O \) iff \( t \equiv 0 \pmod q \) or \( \sigma_1 = O \). Both are excluded by hypothesis (\( t \in \mathbb{Z}_q^\ast \) and \( \sigma_1 \neq O \) by PS validity). Therefore \( \sigma'_1 \neq O \) and the pair satisfies the full PS validity predicate. \( \blacksquare \)
Implementation guard. The registration contract MUST enforce \( \sigma'_1 \neq O \). Without this check, a prover can submit \( \sigma' = (O, O) \) (obtainable by picking \( t = 0 \)); the pairing equation then reduces to \( 1 = 1 \) in \( G_T \) for every \( m \), allowing a fabricated credential to pass registration. Theorem 1's completeness argument explicitly depends on \( t \neq 0 \); the contract's guard is the runtime witness of this hypothesis.
Theorem 2 (Statistical Unlinkability)
Statement. Let \( \sigma \) be any fixed valid PS signature on \( m \). If \( t \) is sampled uniformly from \( \mathbb{Z}_q^\ast \), then the distribution of \( \sigma' = (t \sigma_1, t \sigma_2) \) is identical to the distribution of \( \text{PS.Sign}(sk_{\text{issuer}}, m) \) for \( h \) sampled uniformly from \( G_1 \setminus \{O\} \), and is independent of \( \sigma \).
Proof. Since \( G_1 \) is cyclic of prime order \( q \) and \( \sigma_1 \neq O \), the map
\[ \varphi_\sigma : \mathbb{Z}_q^\ast \to G_1 \setminus \{O\}, \qquad t \mapsto t \sigma_1 \]
is a bijection (it is the restriction to non-zero scalars of the group isomorphism \( \mathbb{Z}_q \to \langle \sigma_1 \rangle = G_1 \)). Hence \( \sigma'_1 \) is uniform on \( G_1 \setminus \{O\} \) regardless of the specific \( \sigma_1 \) we started with.
Given \( \sigma'_1 = h' \), the second component is forced: \( \sigma'_2 = t \sigma_2 = t (x + m y) \sigma_1 = (x + m y) (t \sigma_1) = (x + m y) h' \). That is exactly what a fresh \( \text{PS.Sign} \) with random \( h' \) would output.
So the joint distribution of \( \sigma' \) matches the fresh-signature distribution, and the map \( \sigma \mapsto \sigma' \) destroys all information about \( \sigma \) beyond the fact that both sign the same \( m \). This is statistical (information-theoretic) unlinkability, not merely computational. \( \blacksquare \)
Remark. The argument uses only that \( G_1 \) has prime order; no cryptographic assumption is invoked. A cofactor \( > 1 \) would leak subgroup membership of \( \sigma_1 \); the prime-order property of BN254 \( G_1 \) is therefore load-bearing.
Sanity Check (py_ecc on BN254)
import secrets, hashlib
from py_ecc.bn128 import bn128_curve as bc
from py_ecc.bn128 import pairing
ORDER = bc.curve_order
G1, G2 = bc.G1, bc.G2
multiply, add, neg, eq = bc.multiply, bc.add, bc.neg, bc.eq
def rand():
return secrets.randbelow(ORDER - 1) + 1
# Issuer key pair
x, y = rand(), rand()
X, Y = multiply(G2, x), multiply(G2, y)
# Alice's identity and PS signature sigma = (h, (x + m y) h)
m = int(hashlib.sha256(b"Alice Johnson | Alberta | AIC-2026").hexdigest(), 16) % ORDER
h = multiply(G1, rand())
sigma = (h, multiply(h, (x + m * y) % ORDER))
# Rerandomize with t != 0
t = rand()
sigma_p = (multiply(sigma[0], t), multiply(sigma[1], t))
# --- Theorem 1: completeness ---
lhs = pairing(add(X, multiply(Y, m)), sigma_p[0])
rhs = pairing(G2, sigma_p[1])
assert lhs == rhs, "Theorem 1 FAILED: rerandomized signature does not verify"
assert not eq(sigma_p[0], bc.Z1), "Theorem 1 guard: sigma'_1 == O"
print("Theorem 1 (completeness): PASS")
# --- Theorem 2: fresh rerandomization looks like a fresh signing ---
# Two independent rerandomizations should be indistinguishable from two
# independent fresh signings. We check both give valid signatures on m
# whose sigma_1 components are independent uniform-looking elements.
t2 = rand()
sigma_p2 = (multiply(sigma[0], t2), multiply(sigma[1], t2))
assert not eq(sigma_p[0], sigma_p2[0]), "distinct rerandomizations collide"
lhs2 = pairing(add(X, multiply(Y, m)), sigma_p2[0])
rhs2 = pairing(G2, sigma_p2[1])
assert lhs2 == rhs2
print("Theorem 2 (unlinkability): PASS (two rerandomizations both verify, distinct)")
# --- Theorem 1 guard: the trivial signature (t = 0) is degenerate ---
zero_sig = (bc.Z1, bc.Z1)
lhs0 = pairing(add(X, multiply(Y, m)), zero_sig[0]) # pairing with O -> 1_GT
rhs0 = pairing(G2, zero_sig[1]) # pairing with O -> 1_GT
# The pairing equation holds for ANY m -- this is why the guard matters
assert lhs0 == rhs0
# But the registration contract rejects it because sigma'_1 == O:
rejected = eq(zero_sig[0], bc.Z1)
assert rejected
print("Theorem 1 guard (sigma'_1 != O): PASS (trivial signature rejected by guard)")Theorem 1 (completeness): PASS Theorem 2 (unlinkability): PASS (two rerandomizations both verify, distinct) Theorem 1 guard (sigma'_1 != O): PASS (trivial signature rejected by guard)
How to read the output: Three PASS lines. The first confirms Theorem 1's pairing identity on a random rerandomization. The second confirms two fresh rerandomizations are distinct and both verify (a minimal demonstration of the unlinkable-yet-valid property). The third demonstrates why the \( \sigma'_1 \neq O \) check is mandatory: the pairing equation is trivially satisfied by \( (O, O) \) for any \( m \), so only the explicit guard prevents this degenerate signature from passing.
Part II: Chaum-Pedersen Re-Encryption Correctness
Setup
Alice holds a credential whose ElGamal ciphertext is registered on chain and was verified at registration time against her PS signature:
\[ E_{\text{alice}} = (R_a, C_a) = (r_a \cdot G,\; M + r_a \cdot pk_{\text{alice}}), \qquad pk_{\text{alice}} = sk_{\text{alice}} \cdot G. \]
To identify herself to Bob she re-encrypts under \( pk_{\text{bob}} \) with fresh randomness \( r_b \):
\[ E_{\text{bob}} = (R_b, C_b) = (r_b \cdot G,\; M + r_b \cdot pk_{\text{bob}}). \]
She then produces a non-interactive Chaum-Pedersen proof that \( E_{\text{alice}} \) and \( E_{\text{bob}} \) encrypt the same message point \( M \) – without revealing \( M \), \( m \), \( sk_{\text{alice}} \), or \( r_b \). The BUCK contract reads \( E_{\text{alice}} \) from storage (not calldata) and verifies the proof.
The Statement
Alice proves knowledge of \( (sk_{\text{alice}}, r_b) \) satisfying the three simultaneous relations
\begin{align} pk_{\text{alice}} &= sk_{\text{alice}} \cdot G \tag{S1} \\ R_b &= r_b \cdot G \tag{S2} \\ C_a - sk_{\text{alice}} \cdot R_a &= C_b - r_b \cdot pk_{\text{bob}}. \tag{S3} \end{align}(S1) binds her to the registered public key; (S2) binds \( r_b \) to the ciphertext she submits; (S3) states that the ElGamal-decrypt of \( E_{\text{alice}} \) equals the ElGamal-decrypt of \( E_{\text{bob}} \) under their respective secret keys.
Protocol (Fiat-Shamir)
Commit. Prover samples \( k_1, k_2 \leftarrow \mathbb{Z}_q \) and computes
\[ T_1 = k_1 \cdot G, \qquad T_2 = k_2 \cdot G, \qquad T_3 = k_2 \cdot pk_{\text{bob}} - k_1 \cdot R_a. \]
Challenge.
\[ e = H\!\left(G,\; pk_{\text{alice}},\; pk_{\text{bob}},\; R_a, C_a, R_b, C_b,\; T_1, T_2, T_3,\; \texttt{msg.sender},\; \texttt{spender},\; \texttt{chainid}\right). \]
Respond. \( s_1 = k_1 - e \cdot sk_{\text{alice}} \pmod q,\qquad s_2 = k_2 - e \cdot r_b \pmod q \).
Verify. The contract recomputes \( e \) from the public inputs and the prover-supplied commitments, then checks
\begin{align} s_1 \cdot G + e \cdot pk_{\text{alice}} &\stackrel{?}{=} T_1 \tag{V1} \\ s_2 \cdot G + e \cdot R_b &\stackrel{?}{=} T_2 \tag{V2} \\ s_2 \cdot pk_{\text{bob}} - s_1 \cdot R_a + e \cdot (C_b - C_a) &\stackrel{?}{=} T_3. \tag{V3} \end{align}Theorem 3 (Completeness)
Statement. An honestly generated transcript satisfies (V1), (V2), (V3).
Proof. (V1) and (V2) are standard Schnorr identities:
\begin{align*} s_1 G + e \cdot pk_{\text{alice}} &= (k_1 - e \cdot sk_{\text{alice}}) G + e \cdot sk_{\text{alice}} G = k_1 G = T_1, \\ s_2 G + e \cdot R_b &= (k_2 - e r_b) G + e r_b G = k_2 G = T_2. \end{align*}For (V3), substitute \( s_1, s_2 \) and rearrange:
\begin{align*} s_2 \cdot pk_{\text{bob}} - s_1 \cdot R_a + e (C_b - C_a) &= (k_2 - e r_b) pk_{\text{bob}} - (k_1 - e \cdot sk_{\text{alice}}) R_a + e (C_b - C_a) \\ &= \bigl[k_2 pk_{\text{bob}} - k_1 R_a\bigr] + e \bigl[sk_{\text{alice}} R_a - r_b \cdot pk_{\text{bob}} + C_b - C_a\bigr] \\ &= T_3 + e \bigl[-\,(C_a - sk_{\text{alice}} R_a) + (C_b - r_b \cdot pk_{\text{bob}})\bigr] \\ &= T_3 + e \cdot \bigl[-M + M\bigr] \\ &= T_3. \end{align*}where the penultimate line uses the witness relations from the honest setup, and the final line uses (S3). \( \blacksquare \)
Theorem 4 (Special Soundness / 2-Extractability)
Statement. Given two accepting transcripts \( (T_1, T_2, T_3,\; e,\; s_1, s_2) \) and \( (T_1, T_2, T_3,\; e',\; s'_1, s'_2) \) with \( e \neq e' \) (and identical public inputs), there is an efficient algorithm that outputs a witness \( (sk^\ast, r^\ast) \) satisfying (S1), (S2), (S3).
Proof (extractor). Subtract (V1) for the two transcripts:
\[ (s_1 - s'_1) G + (e - e') \cdot pk_{\text{alice}} = O \quad \Longrightarrow \quad pk_{\text{alice}} = \frac{s'_1 - s_1}{e - e'} \cdot G. \]
Set \( sk^\ast := (s'_1 - s_1)(e - e')^{-1} \bmod q \); this is well-defined because \( q \) is prime and \( e \neq e' \). By construction (S1) holds. Similarly from (V2), \( r^\ast := (s'_2 - s_2)(e - e')^{-1} \bmod q \) satisfies (S2): \( R_b = r^\ast G \).
For (S3), subtract (V3) for the two transcripts:
\[ (s_2 - s'_2) \cdot pk_{\text{bob}} - (s_1 - s'_1) \cdot R_a + (e - e')(C_b - C_a) = O. \]
Substitute \( s_2 - s'_2 = -(e - e') r^\ast \) and \( s_1 - s'_1 = -(e - e') sk^\ast \):
\[ -(e - e') r^\ast \cdot pk_{\text{bob}} + (e - e') sk^\ast \cdot R_a + (e - e')(C_b - C_a) = O. \]
Dividing through by the nonzero scalar \( (e - e') \):
\[ sk^\ast \cdot R_a - r^\ast \cdot pk_{\text{bob}} + (C_b - C_a) = O \qquad \Longleftrightarrow \qquad C_a - sk^\ast \cdot R_a = C_b - r^\ast \cdot pk_{\text{bob}}, \]
which is (S3). Therefore \( (sk^\ast, r^\ast) \) is a valid witness, and the extractor runs in the time of four field inversions and a constant number of group operations. \( \blacksquare \)
Remark (implication for cheating provers). Special soundness lifts, via the forking lemma2, to a reduction from any efficient prover that convinces the verifier on a false statement to an efficient solver for discrete logarithms on \( G_1 \). Under the DLog assumption on \( \text{alt\_bn128} \), no such prover exists. In particular, Alice cannot produce a valid proof for \( E_{\text{bob}} \) encrypting any \( M' \neq M \) – not merely "hard to compute," but infeasible under a standard assumption Ethereum already requires.
Theorem 5 (Honest-Verifier Zero-Knowledge)
Statement. There is an efficient simulator \( \mathcal{S} \) that, given only the public inputs and a challenge \( e \) drawn from the verifier's distribution, outputs a transcript \( (T_1, T_2, T_3, e, s_1, s_2) \) distributed identically to an honest prover's transcript conditioned on that challenge.
Proof (simulator). \( \mathcal{S} \) samples \( s_1, s_2 \leftarrow \mathbb{Z}_q \) uniformly, then defines
\begin{align*} T_1 &:= s_1 \cdot G + e \cdot pk_{\text{alice}}, \\ T_2 &:= s_2 \cdot G + e \cdot R_b, \\ T_3 &:= s_2 \cdot pk_{\text{bob}} - s_1 \cdot R_a + e \cdot (C_b - C_a). \end{align*}Equations (V1), (V2), (V3) hold by construction, so the transcript is accepting.
Distribution match. In the real protocol, \( k_1, k_2 \) are uniform on \( \mathbb{Z}_q \); the honest responses \( s_i = k_i - e \cdot w_i \) are therefore uniform on \( \mathbb{Z}_q \) (shift by a constant). The commitments \( T_1, T_2, T_3 \) are deterministic functions of the \( s_i \) and the public inputs: (V1), (V2), (V3) each uniquely determine one \( T_i \) from the corresponding equation. The simulator samples \( s_i \) uniformly and computes the \( T_i \) via the same equations – the joint distribution over \( (T_1, T_2, T_3, s_1, s_2) \) is identical. \( \blacksquare \)
Corollary (NIZK via Fiat-Shamir in the ROM). Model \( H \) as a programmable random oracle. The Fiat-Shamir transform replaces the verifier's challenge with \( e = H(\text{public inputs}, T_1, T_2, T_3, \text{context}) \). Standard arguments3 promote:
- HVZK \( \Rightarrow \) NIZK (the simulator programs \( H \) so that \( H(\text{public inputs}, T_1, T_2, T_3, \ldots) = e \) for the \( (T_i) \) it produced);
- special soundness \( \Rightarrow \) proof-of-knowledge soundness (the forking lemma rewinds the oracle).
The binding of \( \texttt{msg.sender}, \texttt{spender}, \texttt{chainid} \) into the hash input is standard domain separation that prevents cross-context replay.
Sanity Check (py_ecc on BN254)
# Build registered credential E_alice for Alice
sk_alice = rand()
pk_alice = multiply(G1, sk_alice)
r_a = rand()
R_a = multiply(G1, r_a)
M = multiply(G1, m) # identity point M = m * G
C_a = add(M, multiply(pk_alice, r_a))
# Bob's identity public key (Alice knows this from the registry)
sk_bob = rand()
pk_bob = multiply(G1, sk_bob)
# Re-encryption for Bob: E_bob = (r_b * G, M + r_b * pk_bob)
r_b = rand()
R_b = multiply(G1, r_b)
C_b = add(M, multiply(pk_bob, r_b))
def cp_prove(k1, k2):
T1 = multiply(G1, k1)
T2 = multiply(G1, k2)
T3 = add(multiply(pk_bob, k2), neg(multiply(R_a, k1)))
tag = "|".join(str(v) for v in
[G1, pk_alice, pk_bob, R_a, C_a, R_b, C_b, T1, T2, T3])
e = int(hashlib.sha256(tag.encode()).hexdigest(), 16) % ORDER
s1 = (k1 - e * sk_alice) % ORDER
s2 = (k2 - e * r_b) % ORDER
return (T1, T2, T3, e, s1, s2)
def cp_verify(T1, T2, T3, e, s1, s2):
v1 = eq(add(multiply(G1, s1), multiply(pk_alice, e)), T1)
v2 = eq(add(multiply(G1, s2), multiply(R_b, e)), T2)
Cdiff = add(C_b, neg(C_a))
v3 = eq(add(add(multiply(pk_bob, s2), neg(multiply(R_a, s1))),
multiply(Cdiff, e)), T3)
return v1, v2, v3
# --- Theorem 3: completeness ---
proof = cp_prove(rand(), rand())
v1, v2, v3 = cp_verify(*proof)
assert v1 and v2 and v3, "Theorem 3 FAILED"
print("Theorem 3 (completeness): PASS")
# --- Theorem 4: extract witness from two transcripts with same (T1,T2,T3) ---
# We fix (k1, k2) to produce the same (T1, T2, T3), then force two
# different challenges by hashing with two different context strings
# (in production the different challenges arise from rewinding the RO).
k1, k2 = rand(), rand()
T1 = multiply(G1, k1)
T2 = multiply(G1, k2)
T3 = add(multiply(pk_bob, k2), neg(multiply(R_a, k1)))
def responses(e):
return (k1 - e * sk_alice) % ORDER, (k2 - e * r_b) % ORDER
e_1 = rand()
e_2 = rand()
while e_1 == e_2:
e_2 = rand()
s1_a, s2_a = responses(e_1)
s1_b, s2_b = responses(e_2)
def inv_mod(a, p):
return pow(a, -1, p)
delta_e = (e_1 - e_2) % ORDER
delta_e_inv = inv_mod(delta_e, ORDER)
sk_star = ((s1_b - s1_a) * delta_e_inv) % ORDER # should equal sk_alice
r_star = ((s2_b - s2_a) * delta_e_inv) % ORDER # should equal r_b
assert sk_star == sk_alice, "Theorem 4 sk extraction FAILED"
assert r_star == r_b, "Theorem 4 r extraction FAILED"
# Witnesses satisfy (S1), (S2), (S3):
assert eq(multiply(G1, sk_star), pk_alice)
assert eq(multiply(G1, r_star), R_b)
assert eq(add(C_a, neg(multiply(R_a, sk_star))),
add(C_b, neg(multiply(pk_bob, r_star))))
print("Theorem 4 (special soundness): PASS (sk, r extracted from two transcripts)")
# --- Theorem 5: simulator produces accepting transcript without witness ---
def cp_simulate(e):
s1 = rand(); s2 = rand()
T1 = add(multiply(G1, s1), multiply(pk_alice, e))
T2 = add(multiply(G1, s2), multiply(R_b, e))
Cdiff = add(C_b, neg(C_a))
T3 = add(add(multiply(pk_bob, s2), neg(multiply(R_a, s1))),
multiply(Cdiff, e))
return (T1, T2, T3, e, s1, s2)
sim = cp_simulate(rand())
v1, v2, v3 = cp_verify(*sim)
assert v1 and v2 and v3, "Theorem 5 FAILED (simulated transcript rejected)"
print("Theorem 5 (HVZK): PASS (simulator accepts without witness)")
# --- Unsoundness counter-example: wrong M at destination ---
M_fake = multiply(G1, rand())
C_b_fake = add(M_fake, multiply(pk_bob, r_b))
# Honest prover tries to produce a proof for (E_alice, (R_b, C_b_fake))
k1, k2 = rand(), rand()
T1 = multiply(G1, k1)
T2 = multiply(G1, k2)
T3 = add(multiply(pk_bob, k2), neg(multiply(R_a, k1)))
tag = "|".join(str(v) for v in
[G1, pk_alice, pk_bob, R_a, C_a, R_b, C_b_fake, T1, T2, T3])
e_f = int(hashlib.sha256(tag.encode()).hexdigest(), 16) % ORDER
s1_f = (k1 - e_f * sk_alice) % ORDER
s2_f = (k2 - e_f * r_b) % ORDER
v1_f = eq(add(multiply(G1, s1_f), multiply(pk_alice, e_f)), T1)
v2_f = eq(add(multiply(G1, s2_f), multiply(R_b, e_f)), T2)
Cdiff_f = add(C_b_fake, neg(C_a))
v3_f = eq(add(add(multiply(pk_bob, s2_f), neg(multiply(R_a, s1_f))),
multiply(Cdiff_f, e_f)), T3)
assert v1_f and v2_f and not v3_f, "Unsoundness: fake M proof should fail at V3"
print("Counter-example (M != M'): V1,V2 PASS; V3 FAIL (as required)")Theorem 3 (completeness): PASS Theorem 4 (special soundness): PASS (sk, r extracted from two transcripts) Theorem 5 (HVZK): PASS (simulator accepts without witness) Counter-example (M != M'): V1,V2 PASS; V3 FAIL (as required)
How to read the output: Four PASS lines plus the counter-example. The counter-example is the mirror image of Theorem 4: when the statement is false (different message points), the first two verification equations still pass (Alice's key and randomness are genuine) but (V3) – the "same-message" equation – fails. This is precisely the mathematical impossibility the prose in the Identity document refers to when it says "no valid proof exists."
Part III: End-to-End Re-Encryption Correctness
Chain of Trust
The on-chain identity pipeline chains together the two primitives proved above:
KYC ceremony: issuer signs m --> sigma on m (q-SDH)
Fountain: rerandomize sigma --> sigma' on same m (Theorem 1, 2)
Registration: PS + NIZK check --> E_alice bound to same m (PS verify + NIZK soundness)
approve(Bob): Alice re-encrypts --> E_bob (Theorem 3)
Chaum-Pedersen --> verified on chain (Theorems 3, 4, 5)
Decryption: Bob computes M --> M = C_b - sk_bob * R_b (ElGamal correctness)
Two-channel: Bob receives --> H(identity_data) * G == M (hash commitment)
Theorem 6 (End-to-End Re-Encryption Correctness)
Statement. Suppose the BUCK contract:
- holds a registered credential \( E_{\text{alice}} = (R_a, C_a) \) together with \( pk_{\text{alice}} \) and a PS signature \( \sigma' \) that verified at registration time against a trusted issuer public key \( (X, Y) \);
- accepts a Chaum-Pedersen proof \( \pi \) supplied by \( \texttt{msg.sender} = \) Alice, with spender \( pk_{\text{bob}} \) and submitted ciphertext \( E_{\text{bob}} = (R_b, C_b) \).
Then, under the DLog assumption on \( G_1 \) and in the ROM, there exists \( M = m \cdot G \) such that \[ C_a - sk_{\text{alice}} \cdot R_a \;=\; M \;=\; C_b - sk_{\text{bob}} \cdot R_b, \] and the scalar \( m \) is the issuer-certified identity scalar covered by \( \sigma' \).
Proof sketch. The registration check evaluates \( e(\sigma'_1, X + m Y) = e(\sigma'_2, g_2) \) together with a NIZK that binds the same \( m \) to \( E_{\text{alice}} \) (so \( C_a - sk_{\text{alice}} R_a = m \cdot G \)); existential unforgeability of PS under q-SDH ensures \( m \) is the scalar the issuer signed. The contract reads \( E_{\text{alice}} \) from storage, not calldata, so the value is frozen.
Theorem 4 (special soundness) plus the forking lemma reduction to DLog gives: from any adversary producing an accepting \( \pi \) with non-negligible probability there is an extractor outputting witnesses \( (sk^\ast, r^\ast) \) with \( C_a - sk^\ast R_a = C_b - r^\ast pk_{\text{bob}} \). Since \( pk_{\text{alice}} = sk^\ast \cdot G = sk_{\text{alice}} \cdot G \) and \( G \) is a generator, \( sk^\ast = sk_{\text{alice}} \); so both sides equal the registered \( M \). Therefore \( E_{\text{bob}} \) decrypts under \( sk_{\text{bob}} \) to the same \( M \) that the issuer's PS signature attests to. \( \blacksquare \)
Why This Suffices for Every Downstream Use Case
approve/transfer(Identity): Bob decrypts \( E_{\text{bob}} \) and obtains \( M \); combined with off-chainidentity_datadelivery and the check \( H(\text{identity\_data}) \cdot G = M \), he learns Alice's real identity with cryptographic assurance.- A-spend predicate (Notes): The recipient-to-depositor binding in addressed Notes (flavors A1 and A2) is the same Chaum-Pedersen statement, lifted inside a SNARK. Theorem 4's soundness lifts directly: a SNARK-verified instance of the Chaum-Pedersen relation implies the same extractable witness relation, so the mint-time recipient credential \( E_{\text{note}} \) and the spend-time depositor credential \( E_{\text{dep}}^{(\text{reg})} \) share \( M \) (formalized as Theorem 8 in Part IV). Identity laundering through the note pool is therefore infeasible.
- Identity Guardians (
approveFor, recovery, cosigning): The Schnorr proof of knowledge of \( sk \) is a one-statement specialization of the above (only (S1) with a Fiat-Shamir binding to \( (\texttt{msg.sender}, \texttt{principal}) \)). Theorems 3-5 apply verbatim with the second and third statements dropped.
Part IV: BUCK Notes Spend Predicates (A1, A2, B1)
This Part proves correctness of the spend predicates underlying BUCK Notes (Notes). A note is an asynchronous deferred approve handshake mediated by a global commitment pool: the issuer's half is encoded in a Merkle commitment at mint time, the depositor's half is supplied at spend time inside a zk-SNARK. The three valid flavors – A1 (addressed cheque, public issuer), A2 (addressed cheque, private issuer), and B1 (bearer cheque, public issuer) – distinguish themselves by the commitment payload but collapse to two spend-circuit shapes:
- A-spend (covers A1 and A2): the depositor proves Chaum-Pedersen equality (Theorem 6) between the recipient-binding ciphertext \( E_{\text{note}} \) baked into the commitment and the depositor's current registered credential, without revealing the encrypted identity scalar \( m \).
- B-spend (covers B1): the depositor proves knowledge of the bearer secret \( \rho \), proves knowledge of their registered \( sk_{\text{dep}} \) via Schnorr PoK, and verifies a Schnorr signature attesting the public issuer's identity.
Throughout, the SNARK proof system (Groth16, PLONK, or any knowledge-sound zk-SNARK over BN254) is treated as a black box. Soundness of the SNARK proof system itself is not analyzed here; we only show that the in-circuit statements – assuming standard SNARK extraction – imply the desired cryptographic guarantees. Soundness of the underlying sigma-protocol relations (CP-equality and Schnorr) is established in Parts I-III; this Part lifts those results to the SNARK setting and adds the new pieces specific to the pool architecture: mint re-randomization correctness (Theorem 7), spend-predicate soundness (Theorems 8, 9), nullifier construction (Theorem 10), and preservation of the BUCK mutual decryptability invariant (Theorem 11).
Setup
Pool state. The BUCK contract maintains
noteTree: an append-only Poseidon Merkle tree of fixed depth 20 (max \( 2^{20} \approx 10^6 \) outstanding commitments) with leaves \[ cm \;=\; \mathrm{Poseidon}_5\bigl(\mathrm{flavor},\; v,\; \rho,\; \mathrm{idHash},\; \mathrm{predicate}\bigr), \] where \( \mathrm{idHash} \) is a Poseidon digest of the per-flavor payload (the recipient ciphertext \( E_{\text{note}} \) for A-spends, the issuer-key bundle for B-spends). Equivalently, writing \( H_{\text{cm}}(\mathrm{flavor},\, v,\, \rho,\, \mathrm{payload},\, \mathrm{predicate}) := \mathrm{Poseidon}_5(\mathrm{flavor},\, v,\, \rho,\, H_{\text{payload}}(\mathrm{payload}),\, \mathrm{predicate}) \). Internal nodes use Poseidon-2 over BN254's scalar field; empty leaves hash to a fixed domain-separatedZERO_VALUE.nullifiers: a set of spent nullifiers.roots: a ring buffer of recentnoteRootvalues (window 30) that spend proofs may pin against.
Mint architecture (Phase 7-bis batch mint). Notes.mint no
longer touches the Merkle tree directly. Instead, each mint
submits a Groth16 proof of in-circuit incremental insertion of
\( N \) fresh leaves – N is fixed per pinned verifier
(currently \( N \in \{16, 32\} \) on L1; see
alberta-buck-notes-rollup-mint.org for the per-N dispatch
architecture and gas-economics rationale). The contract verifies
the proof, asserts oldRoot matches the live root and
nextLeafIndex matches the live size, accepts the SNARK-attested
newRoot into the root-history ring buffer, advances
nextLeafIndex by \( N \), and pulls totalFace BUCK from the
issuer. Per-leaf range checks (\( v_i, \texttt{totalFace} \in
[0, 2^{128}) \)) and value conservation (\( \sum_i v_i ===
\texttt{totalFace} \)) are enforced inside the same circuit.
The pool's BUCK balance and accumulated BUCK-age are tracked by the standard ERC-20 ledger; demurrage arithmetic is orthogonal to the cryptographic predicate and is not analyzed here.
Identity primitives (recap). Each registered identity has
- an issuer-certified scalar \( m \in \mathbb{Z}_q \) and identity point \( M = m \cdot G \in G_1 \) (PS-signed at registration; see Part I);
- an ElGamal credential \( E^{(\text{reg})} = (R^{\text{reg}}, C^{\text{reg}}) = (r G,\; M + r \cdot pk) \) on file in IdentityRegistry, where \( pk = sk \cdot G \).
Identity Fountain re-issuance gives a fresh \( (sk', pk', E^{(\text{reg})\prime}) \) bound to the same \( M \).
Mint construction. Let \( E_r = (R_r, C_r) = (r_r G,\; M_{\text{rec}} + r_r \cdot pk_{\text{rec}}) \) be the recipient's registered credential (read by the issuer at mint time). Let \( r' \leftarrow \mathbb{Z}_q^\ast \) be fresh issuer-side randomness. Define \[ E_{\text{note}} \;:=\; \bigl(R_r + r' G,\;\; C_r + r' \cdot pk_{\text{rec}}\bigr). \] The mint payloads, by flavor, are:
| Flavor | Payload |
|---|---|
| A2 | \( (E_{\text{note}},\ E_{\text{iss-for-rec}}) \) |
| A1 | \( (E_{\text{note}},\ m_{\text{iss}},\ \sigma_{\text{iss}}) \) |
| B1 | \( (m_{\text{iss}},\ \sigma_{\text{iss}},\ pk_{\text{iss}}) \) |
where \( E_{\text{iss-for-rec}} \) is an ElGamal encryption of \( M_{\text{iss}} \) (and a means to recover \( pk_{\text{iss}} \), either by carrying it alongside or by IdentityRegistry lookup of \( m_{\text{iss}} \)) under \( pk_{\text{rec}} \), and \( \sigma_{\text{iss}} \) is a Schnorr signature on \( cm \) under \( pk_{\text{iss}} = m_{\text{iss}} \cdot G \).
Nullifier construction. With domain separators \( \{A, B\} \) and Poseidon-modelled-as-RO \( H_{\text{nf}} \): \[ nf^{(A)} \;=\; H_{\text{nf}}(A \,\|\, M_{\text{rec}} \,\|\, \rho), \qquad nf^{(B)} \;=\; H_{\text{nf}}(B \,\|\, \rho). \]
Design note. An earlier draft of alberta-buck-notes.org keyed
the A-spend nullifier on \( sk_{\text{dep}} \) rather than on
\( M_{\text{rec}} \). Keying on \( M_{\text{rec}} \) – the
issuer-certified identity point, recovered in-circuit by ElGamal
decryption of \( E_{\text{note}} \) – keeps the nullifier
independent of which `sk` value was used to decrypt (a privacy
improvement over sk-keyed nullifiers) and prevents a recipient
who somehow holds two simultaneously-valid credentials for the
same \( M \) from double-depositing. It does not provide
key-loss recovery. The spend-time decryption step still
requires the specific \( sk_{\text{rec}} \) paired with the
mint-time \( pk_{\text{rec}} \); if that key is lost, the note
is unspendable regardless of Identity re-issuance. The notes
document has been updated to match; see Theorem 8 below for the
formal statement and Section "Key-Pair Binding and Loss Semantics"
in alberta-buck-notes.org for the operational implications.
Theorem 7 (Mint Re-randomization Correctness)
Statement. Let \( E_r = (R_r, C_r) = (r_r G,\ M_{\text{rec}} + r_r \cdot pk_{\text{rec}}) \) be a valid ElGamal ciphertext of \( M_{\text{rec}} \) under \( pk_{\text{rec}} \), and let \( r' \in \mathbb{Z}_q \) be arbitrary. Then \( E_{\text{note}} := (R_r + r' G,\; C_r + r' \cdot pk_{\text{rec}}) \) is a valid ElGamal ciphertext of the same \( M_{\text{rec}} \) under the same \( pk_{\text{rec}} \), with effective randomness \( r_r + r' \).
Proof. By group arithmetic on \( G_1 \):
\begin{align*} R_r + r' G &= r_r G + r' G = (r_r + r') G, \\ C_r + r' \cdot pk_{\text{rec}} &= M_{\text{rec}} + r_r \cdot pk_{\text{rec}} + r' \cdot pk_{\text{rec}} \\ &= M_{\text{rec}} + (r_r + r') \cdot pk_{\text{rec}}. \end{align*}The pair has the canonical ElGamal form \( (\rho G,\ M_{\text{rec}} + \rho \cdot pk_{\text{rec}}) \) with \( \rho = r_r + r' \). \( \blacksquare \)
Information-theoretic remarks.
- Issuer ignorance. The issuer manipulates \( E_r \) as group elements only – she never decrypts. She therefore learns neither \( M_{\text{rec}} \), \( m_{\text{rec}} \), nor \( r_r \). This is load-bearing for Theorem 10 (issuer-cannot-precompute the A-spend nullifier) and for the BUCK privacy story (an issuer cannot enumerate registered identities by re-randomizing their credentials).
- Distribution. If \( r' \leftarrow \mathbb{Z}_q^\ast \) is uniform, then \( r_r + r' \) is uniform on \( \mathbb{Z}_q \) (translation by a fixed value). \( E_{\text{note}} \) is therefore distributed identically to a fresh ElGamal encryption of \( M_{\text{rec}} \) under \( pk_{\text{rec}} \) – the same information-theoretic argument as Theorem 2's signature rerandomization, applied to the ciphertext. Two independently-minted A-notes for the same recipient are unlinkable to anyone who does not hold \( sk_{\text{rec}} \).
Theorem 8 (A-Spend Soundness)
Statement. Suppose the BUCK contract accepts a SNARK proof
\( \pi^{(A)} \) for the A-spend circuit
(alberta-buck-notes.org, "A-Spend Circuit") on public inputs
\( (\texttt{noteRoot},\ nf,\ v,\ \texttt{depositorRegistryEntry}) \).
Then under (i) knowledge soundness of the SNARK system,
(ii) the DLog assumption on \( G_1 \), and (iii) the ROM, there
exists an extractor \( \mathcal{E} \) that outputs a witness
\( (\text{flavor},\ v,\ \rho,\ \text{payload},\ \text{path},\
sk_{\text{dep}}) \) such that:
- \( cm = H_{\text{cm}}(\text{flavor},\ v,\ \rho,\ \text{payload}) \)
lies on the Merkle path
pathundernoteRoot. - The extracted \( sk_{\text{dep}} \) satisfies
\( pk_{\text{dep}} = sk_{\text{dep}} \cdot G \), where
\( pk_{\text{dep}} \) is registered in IdentityRegistry under
depositorRegistryEntry. - \( \texttt{payload} \) contains a ciphertext \( E_{\text{note}} = (R_n, C_n) \), and the depositor's identity point \( M := C^{\text{reg}} - sk_{\text{dep}} \cdot R^{\text{reg}} \) (decrypted from their registered credential) satisfies \( C_n - sk_{\text{dep}} \cdot R_n = M \).
- \( nf = H_{\text{nf}}(A \,\|\, M \,\|\, \rho) \).
- If \( \text{flavor} = A1 \): the payload also contains \( (m_{\text{iss}}, \sigma_{\text{iss}}) \) with \( \sigma_{\text{iss}} \) verifying as a Schnorr signature on \( cm \) under \( pk_{\text{iss}} := m_{\text{iss}} \cdot G \).
Proof. Knowledge soundness of the SNARK gives an extractor producing witnesses satisfying every constraint of the circuit (with overwhelming probability). Constraints 1, 2, 4, and 5 are direct algebraic identities on the extracted witness.
Constraint 3 is the in-circuit Chaum-Pedersen equality \[ C_n - sk_{\text{dep}} \cdot R_n \;=\; C^{\text{reg}} - sk_{\text{dep}} \cdot R^{\text{reg}}. \] Theorem 4 (special soundness of Chaum-Pedersen) establishes that any accepting CP-equality statement implies the two ciphertexts share the same encrypted message point under their respective public keys. In the present case both are encrypted under \( pk_{\text{dep}} \) (the depositor's registered key), so equality of decrypted plaintexts reduces to equality in \( G_1 \) – precisely constraint 3.
By the registration-time PS-signature binding (Theorem 6, chain of trust), \( M \) is the issuer-certified identity point covered by the depositor's PS signature at IdentityRegistry registration. By Theorem 7, \( E_{\text{note}} \) was constructed at mint as a re-randomization of the recipient's registered credential and therefore encrypts \( M_{\text{rec}} \). CP-equality forces \( M = M_{\text{rec}} \). \( \blacksquare \)
Implications.
- Wrong identity rejected. No prover lacking knowledge of an \( sk_{\text{dep}} \) bound (by registration) to the same \( m_{\text{rec}} \) chosen at mint can produce an accepting \( \pi^{(A)} \). An adversary holding a credential bound to \( M' \neq M_{\text{rec}} \) fails CP-equality: their \( M = M' \neq M_{\text{rec}} \).
- Issuer cannot self-redeem an A-note. The issuer knows \( pk_{\text{rec}} \) and \( r' \) but not \( sk_{\text{rec}} \); she cannot decrypt \( E_{\text{note}} \) to verify \( M_{\text{rec}} \) and cannot supply an \( sk_{\text{dep}} \) bound to that \( M_{\text{rec}} \) unless she is the recipient (degenerate "self-wormhole" subcase where issuer \( \equiv \) recipient).
- A-notes are bound to the mint-time key pair. The circuit
uses a single witness \( sk_{\text{dep}} \) to decrypt both
the note's \( E_{\text{note}} = (R_n, C_n) \) (encrypted under
\( pk_{\text{rec}} \) at mint time) and the depositor's
currently-registered \( E^{(\text{reg})} \) (encrypted under
the depositor's current \( pk_{\text{dep}} \)). Constraint 3
therefore forces \( pk_{\text{rec}} = pk_{\text{dep}} \): the
same public key must appear on both ciphertexts, otherwise
\( C_n - sk_{\text{dep}} \cdot R_n = M_{\text{rec}} +
(r_r + r') \cdot (pk_{\text{rec}} - pk_{\text{dep}}) \neq
M_{\text{rec}} \). Identity re-issuance produces a fresh
\( (sk', pk') \) bound to the same \( M \) but revokes the
previous credential (
identity.org, Attack 7); the old IdentityRegistry entry failsisVerifiedand cannot be used, while the new entry has \( pk' \neq pk_{\text{rec}} \) and fails CP-equality. Consequently, loss of \( sk_{\text{rec}} \) renders the A-note unspendable. Key-management practice for A-note recipients (especially long-hold A2 cold-storage instruments) must treat \( sk_{\text{rec}} \) as load-bearing for note recoverability.
Theorem 9 (B-Spend Soundness)
Statement. Suppose the BUCK contract accepts a SNARK proof \( \pi^{(B)} \) for the B-spend circuit on public inputs \( (\texttt{noteRoot},\ nf,\ v,\ \texttt{issuerPkClaim},\ \texttt{depositorRegistryEntry}) \). Then under knowledge soundness of the SNARK, the DLog assumption on \( G_1 \), the EUF-CMA security of Schnorr signatures under DLog and the ROM4, and the ROM, there exists an extractor outputting \( (v,\ \rho,\ \text{payload},\ \text{path},\ sk_{\text{dep}}) \) such that:
- \( cm = H_{\text{cm}}(B1,\ v,\ \rho,\ \text{payload}) \) lies
on
pathundernoteRoot. - \( nf = H_{\text{nf}}(B \,\|\, \rho) \).
- \( \texttt{payload} \) contains \( (m_{\text{iss}}, \sigma_{\text{iss}}) \), and \( \sigma_{\text{iss}} \) is a valid Schnorr signature on \( cm \) under \( pk_{\text{iss}} := m_{\text{iss}} \cdot G \), with \( pk_{\text{iss}} \) registered in IdentityRegistry.
- \( pk_{\text{dep}} = sk_{\text{dep}} \cdot G \) is registered
in IdentityRegistry under
depositorRegistryEntry. - \( v = \texttt{faceValue} \).
Proof. The SNARK extractor produces witnesses satisfying every
circuit constraint. Constraint 4 of the B-spend circuit
(alberta-buck-notes.org, "B-Spend Circuit") is a Schnorr proof
of knowledge of \( sk_{\text{dep}} \) for \( pk_{\text{dep}} \) –
a one-statement specialization of Chaum-Pedersen (drop relations
(S2), (S3) of Part II), inheriting Theorem 4's special-soundness
extractor and Theorem 5's HVZK simulator. Constraint 3 is the
verification of the Schnorr signature \( \sigma_{\text{iss}} \) on
\( cm \) under \( pk_{\text{iss}} \); Schnorr signatures are
EUF-CMA secure under DLog and the ROM4, so the
signature could only have been produced by the holder of
\( sk_{\text{iss}} = m_{\text{iss}} \). Constraints 1, 2, 5 are
direct algebraic identities. \( \blacksquare \)
Implications.
- Registered depositor. Every B-spend deposit attaches a registered Identity to the deposit transaction, satisfying the BUCK compliance hook (KYC-attached identity at every transfer endpoint).
- Public issuer attestation. The issuer of a B1 note is publicly attested by \( m_{\text{iss}} \) and the EUF-CMA-secure signature; downstream observers and regulators can audit which public issuer printed the note.
- Knowledge of \( \rho \) is the bearer property. The predicate is satisfied by any registered identity that knows \( \rho \). This is exactly the cashier's-cheque semantics: possession of the opening conveys spend authority.
Remark on issuer race. The issuer who chose \( \rho \) at mint trivially knows it and can race the bearer to deposit – this is a civil-trust property, not a cryptographic vulnerability. The deposit event carries \( E_{\text{dep-for-iss}} \), so an issuer who steals their own bearer cheque is identified at the moment of theft (Theorem 11). The cryptography places no prior restraint on the issuer; the legal and reputational system does. The same trust model applies to a paper cashier's cheque, where the issuing bank could in principle redirect the draft.
Theorem 10 (Nullifier Construction and Double-Spend Prevention)
Statement (A-spend nullifier, keyed by \( M_{\text{rec}} \)).
- Determinism. \( nf^{(A)} = H_{\text{nf}}(A \,\|\, M_{\text{rec}} \,\|\, \rho) \) depends on the depositor's witness only through \( (M_{\text{rec}}, \rho) \). Keying on the identity point rather than on \( sk_{\text{dep}} \) (a) keeps the nullifier value independent of which specific signing key was used to decrypt \( E_{\text{note}} \) – a modest privacy improvement – and (b) prevents any pathological scenario in which a recipient holds multiple simultaneously-valid credentials for the same \( M \) from yielding distinct nullifiers and enabling double-deposit.
- Issuer non-computability (in the ROM). The issuer holds \( pk_{\text{rec}}, r_r, r' \) and \( \rho \), but not \( sk_{\text{rec}} \). Computing \( nf^{(A)} \) requires \( M_{\text{rec}} = C_r - r_r \cdot pk_{\text{rec}} \) (equivalent to ElGamal-decrypting \( E_r \) without \( sk_{\text{rec}} \)) – breaking ElGamal IND-CPA, which reduces to DLog on \( pk_{\text{rec}} \). Hence the issuer cannot precompute the nullifier and observe its on-chain appearance to detect a spend.
- Recipient computability (key-dependent). The recipient decrypts \( E_{\text{note}} \) with the specific \( sk_{\text{rec}} \) paired with the mint-time \( pk_{\text{rec}} \) to recover \( M_{\text{rec}} \), reads \( \rho \) from the (decrypted) commitment opening, and computes \( nf^{(A)} \) – exactly the data the spend circuit requires. Loss of \( sk_{\text{rec}} \) is unrecoverable: no alternative credential can decrypt \( E_{\text{note}} \), so the nullifier – and hence the spend – cannot be produced even if Identity re-issuance preserves \( M_{\text{rec}} \) on a fresh keypair. The nullifier-value invariance across re-issuance is a formal property of the hash function, not an operational recovery mechanism.
Statement (B-spend nullifier, keyed by \( \rho \) alone).
- Public derivability. Any party knowing \( \rho \) can compute \( nf^{(B)} = H_{\text{nf}}(B \| \rho) \). This is precisely the first-come-first-served property of bearer cheques: whoever publishes \( \rho \) first wins.
Statement (double-spend prevention, both shapes).
- Two accepting spend proofs on the same \( cm \) by the same flavor produce the same nullifier (by 1 or by 4); the contract's check \( nf \notin \texttt{nullifiers} \) rejects the second submission.
Statement (cross-flavor disjointness).
- Domain separation (\( A \) vs. \( B \) prefix) makes \( \{nf^{(A)}\} \cap \{nf^{(B)}\} = \emptyset \) except on a negligible RO-collision event. A single \( \texttt{nullifiers} \) set safely tracks both flavors.
Proof sketches. Determinism (1) and public derivability (4) are syntactic from the PRF definition. Issuer non-computability (2) is the IND-CPA reduction outlined above; making the random oracle programmable, an issuer who outputs the correct \( nf^{(A)} \) on \( (pk_{\text{rec}}, E_r, r', \rho) \) with non-negligible probability allows the simulator to read \( M_{\text{rec}} \) off the RO-query log and thereby decrypt \( E_r \). Double-spend rejection (5) is a contract-level invariant on the \( \texttt{nullifiers} \) set. Cross-flavor disjointness (6) follows from RO collision resistance. \( \blacksquare \)
Theorem 11 (Mutual-Decryptability Preservation)
Statement. For every accepted spend (A1, A2, or B1):
- The depositor learns the issuer's identity scalar \( m_{\text{iss}} \) (and thus \( M_{\text{iss}} \), and via off-chain identity-data lookup the issuer's real-world identity).
- The issuer learns the depositor's identity scalar \( m_{\text{dep}} \) (and thus \( M_{\text{dep}} \), and via the same identity-data path the depositor's real-world identity).
This is the BUCK Identity invariant – every transfer's two counterparties mutually disclose – preserved under the Notes pool detour.
Proof (by cases).
- A1, B1 (public issuer). The mint payload contains \( m_{\text{iss}} \) in plaintext, witnessed by \( \sigma_{\text{iss}} \) under \( pk_{\text{iss}} = m_{\text{iss}} \cdot G \). The depositor reads it directly. EUF-CMA security of Schnorr (Theorem 9 citation) guarantees the issuer claim cannot be forged.
- A2 (private issuer). The mint payload contains \( E_{\text{iss-for-rec}} \), an ElGamal encryption of \( M_{\text{iss}} \) under \( pk_{\text{rec}} \) (with \( pk_{\text{iss}} \) similarly encrypted, or recovered from IdentityRegistry by lookup of \( m_{\text{iss}} \) once decrypted). The recipient decrypts using \( sk_{\text{rec}} \) by ElGamal correctness.
- Depositor \( \to \) issuer (all flavors). The deposit transaction emits an event payload \( E_{\text{dep-for-iss}} \), an ElGamal encryption of \( M_{\text{dep}} \) under \( pk_{\text{iss}} \). The issuer decrypts using \( sk_{\text{iss}} \). For A1 and B1 the issuer is public so \( pk_{\text{iss}} \) is directly known to the depositor; for A2 the depositor recovers \( pk_{\text{iss}} \) via the payload-decryption path above.
In each case both parties recover the other's identity point and – via the standard off-chain identity_data delivery path (Theorem 6) – the underlying real-world identity. The on-chain observer learns neither \( M_{\text{iss}} \) (for A2), \( M_{\text{dep}} \), nor the linkage between \( cm \) and the deposit event without solving DLog on the relevant ElGamal keys or guessing \( \rho \) (a uniform \( q \)-element). \( \blacksquare \)
Privacy corollary. Bilateral disclosure between the two
counterparties is preserved. The "mass-harvest" threat – bulk
on-chain analytics, KYC-provider subpoena scope expansion,
chain-analytics enrichment – is broken: the chain shows
\( cm \), \( nf \), public flavor metadata, and the deposit
event, but cannot link any pair without the corresponding
counterparty secret. See alberta-buck-notes.org, "Privacy:
Defeating Mass Harvest, Preserving Bilateral Disclosure," for
the threat-model framing.
Sanity Check (py_ecc on BN254)
# Reuse ORDER, G1, multiply, add, neg, eq, rand, hashlib, m, sk_alice,
# pk_alice, sk_bob, pk_bob from Parts I-III. Issuer = Alice; Bob is
# the recipient, with a fresh registered identity scalar m_rec.
m_rec = int(hashlib.sha256(b"Bob Recipient | Alberta | AIC-2026").hexdigest(),
16) % ORDER
M_rec = multiply(G1, m_rec)
r_r = rand()
R_r = multiply(G1, r_r)
C_r = add(M_rec, multiply(pk_bob, r_r)) # Bob's ElGamal credential
def elgamal_decrypt(R, C, sk):
return add(C, neg(multiply(R, sk)))
assert eq(elgamal_decrypt(R_r, C_r, sk_bob), M_rec)
# --- Theorem 7: mint re-randomization correctness (A2/A1) ---
r_prime = rand()
R_n = add(R_r, multiply(G1, r_prime))
C_n = add(C_r, multiply(pk_bob, r_prime))
assert eq(elgamal_decrypt(R_n, C_n, sk_bob), M_rec), \
"Theorem 7 FAILED: E_note must decrypt to M_rec"
print("Theorem 7 (mint re-randomization): PASS (E_note -> M_rec)")
# --- Theorem 8: A-spend CP-equality holds for the recipient ---
def cp_a_check(R_n, C_n, R_reg, C_reg, sk_dep):
return eq(elgamal_decrypt(R_n, C_n, sk_dep),
elgamal_decrypt(R_reg, C_reg, sk_dep))
assert cp_a_check(R_n, C_n, R_r, C_r, sk_bob)
print("Theorem 8 (A-spend, recipient deposits): PASS (CP M_note=M_reg)")
# --- Theorem 8 (negative): issuer Alice cannot self-redeem ---
M_iss = multiply(G1, m) # Alice's identity point
M_alice_decrypts_note = elgamal_decrypt(R_n, C_n, sk_alice)
assert not eq(M_alice_decrypts_note, M_iss), \
"Issuer self-redeem must fail: decrypt(E_note, sk_alice) != M_iss"
print("Theorem 8 (issuer cannot redeem own A-note): PASS (CP fails)")
# --- Theorem 10.1: nullifier determinism and key-pair binding ---
def H_nf(domain, *fields):
blob = domain + b"|" + b"|".join(str(f).encode() for f in fields)
return int(hashlib.sha256(blob).hexdigest(), 16) % ORDER
rho = rand()
# Recipient with mint-time sk_rec decrypts E_note -> M_rec and
# computes the nullifier. This is the only path that works.
M_rec_decrypted = elgamal_decrypt(R_n, C_n, sk_bob)
assert eq(M_rec_decrypted, M_rec), \
"Theorem 10.1 FAILED: sk_rec must decrypt E_note to M_rec"
nf_A = H_nf(b"A", M_rec_decrypted, rho)
# Nullifier value is a pure function of (M_rec, rho): recomputing
# with the same inputs yields the same value regardless of which
# credential is held. This is a hash-determinism property.
assert nf_A == H_nf(b"A", M_rec, rho), \
"Theorem 10.1 FAILED: nullifier must be deterministic in (M_rec, rho)"
print("Theorem 10.1a (nullifier determinism in (M_rec,rho)): PASS")
# --- Theorem 10.1 (negative): re-issued recipient without sk_rec ---
# Bob re-issues (fresh sk_bob_2, pk_bob_2) bound to the same M_rec.
# Bob no longer has sk_bob. He cannot decrypt E_note:
sk_bob_2 = rand()
pk_bob_2 = multiply(G1, sk_bob_2)
M_wrong = elgamal_decrypt(R_n, C_n, sk_bob_2)
assert not eq(M_wrong, M_rec), \
"Theorem 10.1 FAILED: sk_bob_2 must NOT decrypt E_note to M_rec"
# With the wrong M, the nullifier he could compute differs from nf_A,
# but more importantly the A-spend circuit's CP-equality constraint
# (C_n - sk*R_n == C_reg - sk*R_reg) cannot be satisfied: E_note is
# under pk_bob (old), E_reg_2 is under pk_bob_2 (new), so no single
# sk witness decrypts both to the same point. The re-issued
# recipient cannot produce an accepting spend proof.
r_r_2 = rand()
R_r_2 = multiply(G1, r_r_2)
C_r_2 = add(M_rec, multiply(pk_bob_2, r_r_2))
# sk_bob_2 decrypts E_reg_2 to M_rec ...
assert eq(elgamal_decrypt(R_r_2, C_r_2, sk_bob_2), M_rec)
# ... but the same sk_bob_2 does NOT decrypt E_note to M_rec:
assert not eq(elgamal_decrypt(R_n, C_n, sk_bob_2),
elgamal_decrypt(R_r_2, C_r_2, sk_bob_2)), \
"Theorem 10.1 FAILED: CP-equality must fail under key rotation"
print("Theorem 10.1b (A-note unspendable after key loss): PASS (CP fails)")
# --- Theorem 10.4: B-spend nullifier publicly derivable from rho ---
nf_B_holder = H_nf(b"B", rho)
nf_B_eve = H_nf(b"B", rho) # Eve only needs rho
assert nf_B_holder == nf_B_eve
assert nf_A_pre != nf_B_holder # cross-flavor disjoint
print("Theorem 10.4 (B-spend nullifier public from rho): PASS")
# --- Theorem 11: mutual decryptability round-trip ---
# A2 mint payload: E_iss-for-rec encrypts M_iss under pk_bob.
r_iss_for_rec = rand()
E_iss_R = multiply(G1, r_iss_for_rec)
E_iss_C = add(M_iss, multiply(pk_bob, r_iss_for_rec))
assert eq(elgamal_decrypt(E_iss_R, E_iss_C, sk_bob), M_iss), \
"Theorem 11 FAILED: recipient must recover M_iss"
# Deposit-event payload: E_dep-for-iss encrypts M_dep under pk_alice.
M_dep = M_rec # depositor = recipient
r_dep_for_iss = rand()
E_dep_R = multiply(G1, r_dep_for_iss)
E_dep_C = add(M_dep, multiply(pk_alice, r_dep_for_iss))
assert eq(elgamal_decrypt(E_dep_R, E_dep_C, sk_alice), M_dep), \
"Theorem 11 FAILED: issuer must recover M_dep"
print("Theorem 11 (mutual decryptability round-trip): PASS")Theorem 7 (mint re-randomization): PASS (E_note -> M_rec) Theorem 8 (A-spend, recipient deposits): PASS (CP M_note=M_reg) Theorem 8 (issuer cannot redeem own A-note): PASS (CP fails) Theorem 10.1a (nullifier determinism in (M_rec,rho)): PASS Theorem 10.1b (A-note unspendable after key loss): PASS (CP fails) Theorem 10.4 (B-spend nullifier public from rho): PASS Theorem 11 (mutual decryptability round-trip): PASS
How to read the output: Seven PASS lines. Theorem 7 confirms the mint construction yields a valid encryption of the recipient's \( M \) under their own \( pk \). The two Theorem 8 lines show the recipient succeeding (positive case) and the issuer failing (negative case): only credentials bound to the same \( M \) as the note can produce a CP-equality witness. Theorem 10.1a is nullifier-value determinism (a pure hash property: same inputs give same output). Theorem 10.1b is the key-pair-binding check: a re-issued recipient holding a fresh \( sk' \) (but lacking the mint-time \( sk_{\text{rec}} \)) cannot satisfy CP-equality, so the note is unspendable – the honest converse of the superseded "key-loss recovery" framing. Theorem 10.4 demonstrates the public derivability of the B-spend nullifier (anyone who knows \( \rho \) can compute it – the bearer-cheque property). Theorem 11 closes the mutual-decryptability loop: issuer-side payload encrypts \( M_{\text{iss}} \) for the recipient, deposit-side payload encrypts \( M_{\text{dep}} \) for the issuer – both decrypt correctly under the holder's \( sk \).
Worked Example: End-to-End A1 Note Transfer (SNARK Relations)
The block above asserts each theorem in isolation. Below is a
single end-to-end run of one A1 note (addressed cheque, public
issuer) from issuance to deposit, structured as the two SNARK
relations the prover would attest to and the contract would
verify – mint_relation and spend_relation – plus a toy
Merkle tree and nullifier set to make the contract's role
explicit. An A2 transfer is the same relation with the
issuer-signature constraint dropped; a B1 transfer drops the
CP-equality constraint and re-keys the nullifier on \( \rho \)
instead of \( M_{\text{rec}} \).
# Reuses ORDER, G1, multiply, add, neg, eq, rand, hashlib, m, sk_alice,
# pk_alice, sk_bob, pk_bob, M_rec, R_r, C_r from prior blocks.
def H_cm(*fields):
blob = b"buck-cm|" + b"|".join(str(f).encode() for f in fields)
return int(hashlib.sha256(blob).hexdigest(), 16) % ORDER
def H_node(l, r):
return int(hashlib.sha256(b"merk|" + str(l).encode() + b"|" +
str(r).encode()).hexdigest(), 16) % ORDER
def H_e(*fields):
return int(hashlib.sha256(b"|".join(str(f).encode()
for f in fields)).hexdigest(), 16) % ORDER
def schnorr_sign(sk, msg):
k = rand(); R = multiply(G1, k)
e = H_e(R, multiply(G1, sk), msg)
return (R, (k - e * sk) % ORDER)
def schnorr_verify(pk, msg, sig):
R, s = sig
e = H_e(R, pk, msg)
return eq(add(multiply(G1, s), multiply(pk, e)), R)
def merkle(leaves, depth=3): # 8-leaf toy tree
layer = list(leaves) + [0] * (2**depth - len(leaves))
layers = [layer]
while len(layers[-1]) > 1:
prev = layers[-1]
layers.append([H_node(prev[2*i], prev[2*i+1])
for i in range(len(prev)//2)])
return layers
def merkle_path(layers, idx):
path = []
for layer in layers[:-1]:
path.append((idx & 1, layer[idx ^ 1])); idx >>= 1
return path
def merkle_root(leaf, path):
h = leaf
for bit, sib in path:
h = H_node(sib, h) if bit else H_node(h, sib)
return h
# --- MINT (A1): Alice issues a 1,000-BUCK note to Bob ---------------
face = 1000
m_iss_pub = m # A1: issuer M is in the clear
r_prime = rand()
R_n = add(R_r, multiply(G1, r_prime)) # E_note = re-randomized E_r
C_n = add(C_r, multiply(pk_bob, r_prime))
rho = rand()
cm = H_cm("A1", face, rho, R_n, C_n, m_iss_pub)
sig_iss = schnorr_sign(m_iss_pub, cm) # A1: sk = m_iss, pk = m_iss * G1
def mint_relation_A1(pub, wit): # what the Mint SNARK proves
face_p, cm_p = pub
R_r_w, C_r_w, pk_rec_w, r_prime_w, rho_w, m_iss_w, sig_w = wit
R_n_w = add(R_r_w, multiply(G1, r_prime_w))
C_n_w = add(C_r_w, multiply(pk_rec_w, r_prime_w))
if H_cm("A1", face_p, rho_w, R_n_w, C_n_w, m_iss_w) != cm_p:
return False
return schnorr_verify(multiply(G1, m_iss_w), cm_p, sig_w)
assert mint_relation_A1(
(face, cm),
(R_r, C_r, pk_bob, r_prime, rho, m_iss_pub, sig_iss))
print("Mint relation (A1): PASS (cm opens; issuer sig verifies)")
# Contract appends cm to noteTree; we put it at slot 3.
leaves = [0]*8; leaves[3] = cm
tree = merkle(leaves, depth=3)
root = tree[-1][0]
path = merkle_path(tree, 3)
# --- SPEND (A-spend): Bob deposits the note -------------------------
sk_dep = sk_bob
R_reg, C_reg = R_r, C_r # Bob's currently registered cred.
M_rec_w = add(C_n, neg(multiply(R_n, sk_dep)))
nf = int(hashlib.sha256(b"buck-nf|A|" + str(M_rec_w).encode()
+ b"|" + str(rho).encode()).hexdigest(), 16) % ORDER
def spend_relation_A1(pub, wit): # what the A-Spend SNARK proves
root_p, nf_p, face_p = pub
(cm_w, rho_w, R_n_w, C_n_w, m_iss_w, sig_w, path_w,
sk_dep_w, R_reg_w, C_reg_w) = wit
# (1) Merkle inclusion of cm in noteTree
if merkle_root(cm_w, path_w) != root_p: return False
# (2) commitment opening binds (face, rho, E_note, m_iss)
if H_cm("A1", face_p, rho_w, R_n_w, C_n_w, m_iss_w) != cm_w: return False
# (3) nullifier keyed on M_rec recovered in-circuit
M_rec_c = add(C_n_w, neg(multiply(R_n_w, sk_dep_w)))
nf_c = int(hashlib.sha256(b"buck-nf|A|" + str(M_rec_c).encode()
+ b"|" + str(rho_w).encode()).hexdigest(), 16) % ORDER
if nf_c != nf_p: return False
# (4) CP-equality: registered credential decrypts to the same M
M_reg_c = add(C_reg_w, neg(multiply(R_reg_w, sk_dep_w)))
if not eq(M_rec_c, M_reg_c): return False
# (5) A1-only: issuer signature on cm verifies under m_iss * G
return schnorr_verify(multiply(G1, m_iss_w), cm_w, sig_w)
pub_spend = (root, nf, face)
wit_bob = (cm, rho, R_n, C_n, m_iss_pub, sig_iss, path,
sk_dep, R_reg, C_reg)
assert spend_relation_A1(pub_spend, wit_bob)
print("Spend relation (A1, recipient): PASS (Merkle + opening + nf + CP + sig)")
# Negative: issuer Alice tries to redeem the note Bob is addressed on.
wit_alice = (cm, rho, R_n, C_n, m_iss_pub, sig_iss, path,
sk_alice, R_reg, C_reg)
assert not spend_relation_A1(pub_spend, wit_alice)
print("Spend relation (issuer self-redeem): PASS (rejected: CP-equality fails)")
# --- Contract: nullifier set rejects double spend -------------------
spent = set()
def deposit(pub, wit):
if not spend_relation_A1(pub, wit): return "reject:proof"
if pub[1] in spent: return "reject:double-spend"
spent.add(pub[1]); return f"accept:{pub[2]}"
assert deposit(pub_spend, wit_bob) == f"accept:{face}"
assert deposit(pub_spend, wit_bob) == "reject:double-spend"
print("Contract (nullifier set rejects replay): PASS")Mint relation (A1): PASS (cm opens; issuer sig verifies) Spend relation (A1, recipient): PASS (Merkle + opening + nf + CP + sig) Spend relation (issuer self-redeem): PASS (rejected: CP-equality fails) Contract (nullifier set rejects replay): PASS
How to read the output: The two relations mint_relation_A1 and
spend_relation_A1 are the precise statements a SNARK proof
system (Groth16 / PLONK on BN254) would attest to in production –
they are written as plain Python predicates here so the reader can
trace each constraint by hand. Mint binds \( cm \) to
\( (\text{face}, \rho, E_{\text{note}}, m_{\text{iss}}) \) and to
the issuer's Schnorr signature; spend re-derives \( cm \) from the
witness, checks Merkle inclusion against the public root, recovers
\( M_{\text{rec}} \) from \( E_{\text{note}} \) under
\( sk_{\text{dep}} \), checks the nullifier against
\( H_{\text{nf}}(A \| M_{\text{rec}} \| \rho) \), proves
CP-equality with the depositor's registered credential, and (A1
only) verifies the issuer signature on \( cm \). The two
negative cases – issuer-self-redeem and double-spend – show the
relation rejecting the only two attacks the SNARK is responsible
for filtering at this layer.
Mint-Circuit Status: Phase 7-bis Closure of the Earlier Open Questions
An earlier draft of this document (v0.4) flagged three open
correctness obligations against the Phase-6 placeholder mint
circuit (circuits/mint.circom). The Phase 7-bis batch-mint
pivot (circuits/mint_batch.circom, deployed as per-N pinned
variants mint_batch_n${N}.circom; see
alberta-buck-notes-rollup-mint.org) closes all three at the
mint layer. This section records the closure for the proof
record so a reader cross-checking the prior draft can see which
obligations are now discharged in the circuit vs. deferred to the
spend circuit.
Closure 1 (Field-Overflow / Value-Conservation Range Check) – Discharged in circuit
The Phase-6 draft's value-conservation constraint
\( \sum_{i=1}^{N} v_i \;===\; \texttt{totalFace} \) is an
equation in \( \mathbb{F}_r \) with \( r \approx 2^{254} \) and
therefore admits integer-overflow witnesses unless each \( v_i \)
is bounded. mint_batch.circom now applies Num2Bits(128) to
every \( v_i \) and to totalFace (constraint family (R) in
the circuit source). With \( N \leq 2^{20} \) (the tree-depth
cap) and 128-bit per-leaf face, the integer sum cannot overflow:
\( N \cdot 2^{128} \leq 2^{148} \ll 2^{254} = r \). No spend
circuit can be tricked into releasing more BUCK than was escrowed
by an integer-vs-field-arithmetic mismatch on the mint side.
Closure 2 (Single transferFrom \(\leftrightarrow\) \( N \) Commitments) – Discharged by SNARK binding
Phase 7-bis makes the totalFace public input of the mint
SNARK both (i) the value passed to buck.transferFrom and
(ii) the value the circuit forces to equal \( \sum_i v_i \).
The single Transfer event of size totalFace is therefore
cryptographically bound to the Groth16 proof's public inputs –
which include the entire cms[] array on the per-N pinned
verifiers (see alberta-buck-notes-rollup-mint.org, "Calldata
delivery"). No separate batch-identifier storage slot is needed
for soundness; on-chain analytics that want a transfer \(\leftrightarrow\) batch
correspondence can recover it from the Minted event's cms
parameter and the simultaneous Transfer.
Closure 3 (Spend-Circuit Trio) – Mint half discharged; nullifier + spend-side value conservation deferred to spend.circom
The earlier draft's three coupled obligations – Merkle membership, nullifier determinism, and value conservation – now split cleanly between the two circuits:
- Merkle membership (mint side).
mint_batchperforms an in-circuit dual Merkle walk per leaf (constraint family (M) in the circuit source): the empty-seat walk constrains every witness sibling against the rolling prior root, and the filled-seat walk advances the rolling root with \( cm_i \) inserted. After all \( N \) leaves the final rolling root is constrained to equal the publicnewRoot. The contract acceptsnewRootinto its root-history ring buffer; spend circuits prove inclusion against any root in that window. The Phase-2 flatcommitments[]array is gone. - Nullifier determinism (spend side). The spend circuit must
still recompute \( nf = H_{\text{nf}}(\mathrm{flavor},\
M_{\text{rec}},\ \rho) \) (A-spend) or \( nf =
H_{\text{nf}}(B,\ \rho) \) (B-spend) from the committed
witness. This is the obligation Theorem 10 states; closure
belongs to
circuits/spend.circom. - Value conservation (spend side). The spend circuit must
still output
faceReleasedequal to the committed \( v \) in the opening, with \( v \) the same 128-bit-bounded quantity range-checked at mint. The mint-side range check (Closure 1) remains load-bearing here: a spend circuit that trusts \( v \) without re-bounding it relies on the mint circuit having already done so.
The mint-circuit layout is now frozen at \( \mathrm{Poseidon}_5(\mathrm{flavor},\, v,\, \rho,\, \mathrm{idHash},\, \mathrm{predicate}) \) over BN254 with tree depth 20; spend-circuit design no longer ratifies the mint-circuit layout because the layout is published and pinned by the per-N verifiers already deployed.
Security Assumptions Summary
| Theorem | Primitive | Assumption needed | Conditional on crypto? |
|---|---|---|---|
| Theorem 1 (PS completeness) | PS | bilinearity of \( e \) | no (unconditional) |
| Theorem 2 (PS unlinkability) | PS | prime-order \( G_1 \), uniform \( t \) | no (information-theoretic) |
| Theorem 3 (CP completeness) | CP | group axioms | no (unconditional) |
| Theorem 4 (CP special soundness) | CP | none for extractor; DLog for | DLog (only for reduction) |
| cheating-prover reduction | |||
| Theorem 5 (CP HVZK) | CP | uniform \( k_1, k_2 \) | no (perfect simulation) |
| NIZK corollary | CP + FS | ROM | ROM |
| Theorem 6 (end-to-end) | PS + CP | q-SDH (PS unforgeability), DLog, ROM | yes |
| Theorem 7 (mint re-randomization) | ElGamal | group axioms | no (unconditional) |
| Theorem 8 (A-spend soundness) | CP-in-SNARK | SNARK knowledge soundness, DLog, ROM | yes |
| Theorem 9 (B-spend soundness) | Schnorr | SNARK knowledge soundness, | yes |
| + Sig + PoK | Schnorr EUF-CMA (DLog + ROM) | ||
| Theorem 10 (nullifier construction) | Poseidon-PRF | ROM (Poseidon), DLog (issuer-ignorance) | ROM, DLog |
| Theorem 11 (mutual decryptability) | ElGamal | ElGamal correctness; Schnorr EUF-CMA | DLog, ROM |
Every assumption on the "yes" rows is already load-bearing for Ethereum itself (ECDSA on secp256k1 assumes DLog; every pairing-based SNARK verifier on L1 uses the same ROM reductions; every existing BLS-based rollup assumes q-SDH-class primitives). Alberta Buck introduces no novel cryptographic assumption.
Implementation Notes
What the Contract Must Enforce
The theorems assume a well-formed verifier. Three conditions on the on-chain code are load-bearing:
- Registration: \( \sigma'_1 \neq O \). Required by Theorem 1's non-degeneracy argument. A missing check admits the trivial signature attack (see Identity doc, Attack 9 / Link 1).
- Registration: \( G_2 \) subgroup check. The
ecPairingprecompile (post-EIP-197) validates that \( X, Y \) lie in the correct prime-order subgroup of \( G_2 \). Without this, small-subgroup attacks on the twist curve could forge pairings. Theorem 1 is stated over the correct subgroup; enforcement is the precompile's responsibility. - Verification: \( E_{\text{alice}} \) read from storage, not calldata. Theorem 6's reduction routes through the registration-time binding of \( E_{\text{alice}} \) to issuer-certified \( m \). If Alice supplies \( E_{\text{alice}} \) via calldata the binding is severed and Theorem 6 does not apply.
What the Prover Must Enforce
- Uniform randomness for \( t, k_1, k_2, r_b \). Theorems 2 and 5 depend on uniform distributions. A biased RNG converts statistical unlinkability into a detectable correlation and converts perfect HVZK into approximate HVZK with an adversarial advantage proportional to the bias.
- Fiat-Shamir domain separation. The hash input must include all public inputs and context (\( \texttt{msg.sender}, \texttt{spender}, \texttt{chainid} \)). Omitting any one opens a cross-context replay path; the forking lemma reduction is unaffected, but the NIZK is no longer bound to the intended transaction.
Scope Not Covered Here
- Knowledge soundness of the registration-time NIZK that binds
\( \sigma' \) to \( E_{\text{alice}} \): this is a standard
Schnorr-family sigma protocol over a linear pairing relation
(see
alberta-buck-identity.org, "One-Time (PS Signature + NIZK Verification at Registration)") and follows the same completeness / soundness / ZK template as Theorems 3-5. A separate write-up at the same level of detail is the natural next step. - Circuit soundness of the BUCK Notes spend circuits: Part IV proves that the in-circuit statements (CP-equality on \( E_{\text{note}} \) for A-spend; Schnorr PoK and signature verify for B-spend) imply the desired cryptographic guarantees assuming knowledge soundness of the underlying SNARK system. Soundness of the proof system itself (Groth16 / PLONK / STARK) depends on the chosen proof system's trusted setup (if any) and circuit correctness; this is orthogonal to the contents of this document.
- Collision-resistance and hiding of the Poseidon commitment hash \( H_{\text{cm}} \) and pseudorandomness of the Poseidon nullifier PRF. Poseidon is modelled as a random oracle in the SNARK circuit for the double-spend and commitment-binding arguments; its concrete security on BN254 has been independently studied and is assumed here. See the Poseidon-hash cryptanalysis literature for current best attacks and parameter choices.
Footnotes
Pointcheval, D. and Sanders, O. "Short Randomizable Signatures." CT-RSA 2016, LNCS 9610, pp. 111-126. Theorem 4.2 establishes EUF-CMA security under the q-SDH assumption in bilinear groups.
Pointcheval, D. and Stern, J. "Security Arguments for Digital Signatures and Blind Signatures." Journal of Cryptology 13(3),
- The forking lemma: an adversary that convinces a sigma-protocol
verifier with non-negligible probability can be rewound to produce two accepting transcripts with distinct challenges, from which the witness is extracted.
Fiat, A. and Shamir, A. "How to Prove Yourself: Practical Solutions to Identification and Signature Problems." CRYPTO 1986. The transform replacing an interactive verifier's challenge with a hash of the transcript, analyzed in the random oracle model of Bellare and Rogaway (CCS 1993).
Schnorr, C.-P. "Efficient Signature Generation by Smart Cards." Journal of Cryptology 4(3), 1991, pp. 161-174. The Schnorr signature scheme is EUF-CMA secure under the discrete logarithm assumption in the random oracle model; see Pointcheval and Stern (2000) 2 for the forking-lemma reduction, and Bellare and Neven, "Multi-signatures in the Plain Public-Key Model" (CCS 2006) for tightened security bounds.