UltrafastSecp256k1 3.68.0
Ultra high-performance secp256k1 elliptic curve cryptography library
Loading...
Searching...
No Matches
zk.hpp
Go to the documentation of this file.
1#ifndef SECP256K1_ZK_HPP
2#define SECP256K1_ZK_HPP
3#pragma once
4
5// ============================================================================
6// Zero-Knowledge Proof Layer for secp256k1
7// ============================================================================
8// Implements ZK proof primitives over the secp256k1 curve:
9//
10// 1. Schnorr Knowledge Proof (sigma protocol)
11// - Non-interactive proof of knowledge of discrete log
12// - Prove: "I know x such that P = x*G" without revealing x
13//
14// 2. DLEQ Proof (Discrete Log Equality)
15// - Prove: log_G(P) == log_H(Q) without revealing the secret
16// - Used in: VRFs, adaptor signatures, ECDH proofs, atomic swaps
17//
18// 3. Bulletproof Range Proof
19// - Prove: committed value v in [0, 2^n) without revealing v
20// - Logarithmic proof size via inner product argument
21// - Used in: Confidential Transactions, Mimblewimble, Liquid
22//
23// Security: All proving operations use CT layer (constant-time).
24// Verification uses fast layer (variable-time, public data).
25//
26// Fiat-Shamir: All proofs are non-interactive via tagged SHA-256 hashing.
27// ============================================================================
28
29#include <array>
30#include <cstdint>
31#include <cstddef>
32#include "secp256k1/scalar.hpp"
33#include "secp256k1/point.hpp"
35
36namespace secp256k1 {
37namespace zk {
38
39// ============================================================================
40// 1. Schnorr Knowledge Proof (Sigma Protocol)
41// ============================================================================
42// Non-interactive proof of knowledge of discrete log.
43// Proves: "I know x such that P = x*G" (or P = x*B for arbitrary base B).
44//
45// Protocol (Fiat-Shamir):
46// Prover: k <- random, R = k*G, e = H("ZK/knowledge" || R || P || msg), s = k + e*x
47// Verifier: s*G == R + e*P
48//
49// Proof size: 64 bytes (R_compressed[33] + s[32] -> optimized to R.x[32] + s[32])
50
52 std::array<std::uint8_t, 32> rx; // R.x (x-coordinate of nonce point)
53 fast::Scalar s; // response scalar
54
55 std::array<std::uint8_t, 64> serialize() const;
56 static bool deserialize(const std::uint8_t* data64, KnowledgeProof& out);
57};
58
59// Prove knowledge of secret x such that pubkey = x*G
60// msg: optional binding message (32 bytes, can be all-zero)
61// aux_rand: 32 bytes entropy for nonce hedging
63 const fast::Point& pubkey,
64 const std::array<std::uint8_t, 32>& msg,
65 const std::array<std::uint8_t, 32>& aux_rand);
66
67// Verify knowledge proof against public key and message
69 const fast::Point& pubkey,
70 const std::array<std::uint8_t, 32>& msg);
71
72// Prove knowledge of secret x such that point = x*base (arbitrary base)
74 const fast::Point& point,
75 const fast::Point& base,
76 const std::array<std::uint8_t, 32>& msg,
77 const std::array<std::uint8_t, 32>& aux_rand);
78
79// Verify knowledge proof against arbitrary base
81 const fast::Point& point,
82 const fast::Point& base,
83 const std::array<std::uint8_t, 32>& msg);
84
85
86// ============================================================================
87// 2. DLEQ Proof (Discrete Log Equality)
88// ============================================================================
89// Proves: log_G(P) == log_H(Q), i.e., P = x*G and Q = x*H for same x.
90//
91// Protocol (Fiat-Shamir):
92// Prover: k <- random, R1 = k*G, R2 = k*H
93// e = H("ZK/dleq" || G || H || P || Q || R1 || R2)
94// s = k + e*x
95// Verifier: s*G == R1 + e*P AND s*H == R2 + e*Q
96//
97// Used in VRFs, DLEQ-based adaptor signatures, provable ECDH.
98// Proof size: 64 bytes (e[32] + s[32])
99
100struct DLEQProof {
101 fast::Scalar e; // challenge
102 fast::Scalar s; // response
103
104 std::array<std::uint8_t, 64> serialize() const;
105 static bool deserialize(const std::uint8_t* data64, DLEQProof& out);
106};
107
108// Prove that log_G(P) == log_H(Q) where P = secret*G and Q = secret*H
109// aux_rand: 32 bytes entropy for nonce hedging
111 const fast::Point& G,
112 const fast::Point& H,
113 const fast::Point& P,
114 const fast::Point& Q,
115 const std::array<std::uint8_t, 32>& aux_rand);
116
117// Verify DLEQ proof
118bool dleq_verify(const DLEQProof& proof,
119 const fast::Point& G,
120 const fast::Point& H,
121 const fast::Point& P,
122 const fast::Point& Q);
123
124
125// ============================================================================
126// 3. Bulletproof Range Proof
127// ============================================================================
128// Proves that a Pedersen commitment C = v*H + r*G commits to v in [0, 2^n).
129// Based on Bulletproofs (Bunz et al., 2018).
130//
131// Proof structure:
132// - A, S: vector commitment points (2 group elements)
133// - T1, T2: polynomial commitment points (2 group elements)
134// - tau_x, mu, t_hat: scalar values (3 scalars)
135// - L[], R[]: inner product argument (2*log2(n) group elements)
136// - a, b: final inner product scalars (2 scalars)
137//
138// For n=64 bits: 2*log2(64) = 12 group elements + 7 scalars = ~620 bytes
139// Verification: O(n) multi-exp (can batch across multiple proofs)
140
141static constexpr std::size_t RANGE_PROOF_BITS = 64;
142static constexpr std::size_t RANGE_PROOF_LOG2 = 6; // log2(64)
143
145 // Vector commitments
146 fast::Point A; // commitment to bits vector
147 fast::Point S; // commitment to blinding vectors
148
149 // Polynomial commitments
150 fast::Point T1; // commitment to t_1 coefficient
151 fast::Point T2; // commitment to t_2 coefficient
152
153 // Scalar responses
154 fast::Scalar tau_x; // blinding for polynomial eval
155 fast::Scalar mu; // aggregate blinding
156 fast::Scalar t_hat; // polynomial evaluation at challenge
157
158 // Inner product argument (log2(n) rounds)
159 std::array<fast::Point, RANGE_PROOF_LOG2> L;
160 std::array<fast::Point, RANGE_PROOF_LOG2> R;
161
162 // Final scalars
165};
166
167// Generate range proof for a Pedersen commitment
168// value: the committed value (must be in [0, 2^64))
169// blinding: the blinding factor used in the commitment
170// commitment: the Pedersen commitment C = value*H + blinding*G
171// aux_rand: 32 bytes of entropy
172RangeProof range_prove(std::uint64_t value,
173 const fast::Scalar& blinding,
174 const PedersenCommitment& commitment,
175 const std::array<std::uint8_t, 32>& aux_rand);
176
177// Verify range proof for a Pedersen commitment
178// Returns true if the proof is valid (committed value is in [0, 2^64))
179bool range_verify(const PedersenCommitment& commitment,
180 const RangeProof& proof);
181
182
183// ============================================================================
184// Generator Vectors (for Bulletproofs)
185// ============================================================================
186// Nothing-up-my-sleeve generators: G_i = H("BP_G" || LE32(i)), H_i = H("BP_H" || LE32(i))
187// Cached after first computation.
188
190 std::array<fast::Point, RANGE_PROOF_BITS> G;
191 std::array<fast::Point, RANGE_PROOF_BITS> H;
192};
193
195
196
197// ============================================================================
198// Batch Operations
199// ============================================================================
200
201// Batch-verify multiple range proofs (more efficient than individual verification)
202// Returns true only if ALL proofs are valid.
204 const RangeProof* proofs,
205 std::size_t count);
206
207// Batch-create Pedersen commitments (performance optimization)
208// values[count], blindings[count] -> commitments_out[count]
209void batch_commit(const fast::Scalar* values,
210 const fast::Scalar* blindings,
211 PedersenCommitment* commitments_out,
212 std::size_t count);
213
214
215// ============================================================================
216// 4. ECDSA-in-SNARK Foreign-Field Witness (eprint 2025/695)
217// ============================================================================
218// Generates all intermediate values needed by a PLONK circuit prover to
219// verify an ECDSA signature over secp256k1 with foreign-field arithmetic.
220//
221// Background (eprint 2025/695, Ambrona, Firsov, Querejeta-Azurmendi):
222// secp256k1 p and n are both larger than common SNARK scalar fields (BN254 r,
223// BLS12-381 r). A PLONK circuit must therefore encode every secp256k1 field
224// element as multiple "limbs" — the foreign-field representation. Using 5×52-
225// bit limbs with tight range bounds reduces the gate count for one ECDSA
226// verification from ~50 000 constraints to ~5 000 (≈10× improvement).
227//
228// This function is the **host-side witness generator**: it computes every
229// intermediate value the PLONK prover needs as private inputs, and returns
230// them in both canonical 32-byte encoding AND in 5×52-bit limb form so that
231// the caller can feed them directly into a PLONK framework (Halo2, Plonky3,
232// Circom, etc.) without any additional decomposition step.
233
234// ── Limb container ──────────────────────────────────────────────────────────
235// Per-value foreign-field representation for PLONK circuits.
236// 5 limbs × 52 bits = 260 bits total, covering the 256-bit secp256k1 prime.
237// Top limb (limbs[4]) uses at most 48 bits when the value is < p (or < n).
238// Each limb fits in uint64_t without overflow — no masking needed at capture.
240 std::uint64_t limbs[5]; // little-endian 52-bit limbs
241};
242
243// ── Witness struct ───────────────────────────────────────────────────────────
244// Complete PLONK prover witness for one secp256k1 ECDSA verification.
245// All scalar/field values are provided in both canonical form (Scalar/Point)
246// AND as 5×52-bit limbs ready for PLONK gate wiring.
247//
248// ECDSA verify steps (Fp = secp256k1 field, Fr = secp256k1 scalar field):
249// 1. s_inv = sig_s^{-1} mod n (Fr)
250// 2. u1 = msg_hash * s_inv mod n (Fr)
251// 3. u2 = sig_r * s_inv mod n (Fr)
252// 4. R = u1*G + u2*pubkey (Fp^2)
253// 5. valid = (R ≠ ∞) AND (R.x mod n == sig_r)
255 // ── public inputs ──────────────────────────────────────────────────────
256 ForeignFieldLimbs msg; // message hash mod n (Fr)
257 ForeignFieldLimbs sig_r; // signature r (Fr)
258 ForeignFieldLimbs sig_s; // signature s (Fr)
259 ForeignFieldLimbs pub_x; // public key P.x (Fp)
260 ForeignFieldLimbs pub_y; // public key P.y (Fp)
261
262 // ── private witness (circuit signals) ─────────────────────────────────
263 ForeignFieldLimbs s_inv; // s^{-1} mod n (Fr)
264 ForeignFieldLimbs u1; // e * s^{-1} mod n (Fr)
265 ForeignFieldLimbs u2; // r * s^{-1} mod n (Fr)
269
270 // ── canonical byte encodings (big-endian) ─────────────────────────────
271 std::array<std::uint8_t, 32> bytes_s_inv;
272 std::array<std::uint8_t, 32> bytes_u1;
273 std::array<std::uint8_t, 32> bytes_u2;
274 std::array<std::uint8_t, 32> bytes_result_x;
275 std::array<std::uint8_t, 32> bytes_result_y;
276 std::array<std::uint8_t, 32> bytes_result_x_mod_n;
277
278 // ── verdict ───────────────────────────────────────────────────────────
279 bool valid; // true iff the ECDSA signature is valid
280};
281
282// Compute the ECDSA-in-SNARK foreign-field witness.
283//
284// msg_hash : 32-byte big-endian message hash (hash of the signed message)
285// pubkey : uncompressed public key point P = d*G
286// sig_r : ECDSA signature r-scalar (must be in [1, n-1])
287// sig_s : ECDSA signature s-scalar (must be in [1, n-1]; accepts high-S)
288//
289// Returns a fully populated EcdsaSnarkWitness.
290// If the signature is invalid, `valid` is false but witness values are still
291// populated (to allow the prover to build a failing-path proof if needed).
293 const std::array<std::uint8_t, 32>& msg_hash,
294 const fast::Point& pubkey,
295 const fast::Scalar& sig_r,
296 const fast::Scalar& sig_s);
297
298
299// ============================================================================
300// 5. BIP340 Schnorr-in-SNARK Foreign-Field Witness
301// ============================================================================
302// Generates all intermediate values needed by a PLONK circuit prover to
303// verify a BIP-340 Schnorr signature over secp256k1.
304//
305// BIP-340 verification in a circuit:
306// 1. Lift R (even Y) from 32-byte R.x
307// 2. Lift P (even Y) from 32-byte x-only pubkey
308// 3. e = H("BIP0340/challenge" || R.x || P.x || msg) mod n
309// 4. R' = s*G - e*P
310// 5. valid = (R' == R) => equivalently, s*G == R + e*P
311//
312// Schnorr is simpler than ECDSA for circuits: no modular inverse, only
313// one multi-scalar multiplication, and the challenge is a plain hash.
314//
315// Like EcdsaSnarkWitness, all values are returned in both canonical
316// 32-byte form and 5×52-bit ForeignFieldLimbs for PLONK/Halo2/Circom.
317
319 // ── public inputs ──────────────────────────────────────────────────
320 ForeignFieldLimbs msg; // message (32 bytes) (Fr)
321 ForeignFieldLimbs sig_r; // R.x (nonce x-coordinate) (Fp)
322 ForeignFieldLimbs sig_s; // s scalar (Fr)
323 ForeignFieldLimbs pub_x; // P.x (x-only pubkey) (Fp)
324
325 // ── private witness (circuit signals) ─────────────────────────────
326 ForeignFieldLimbs r_y; // R.y (lifted, even Y) (Fp)
327 ForeignFieldLimbs pub_y; // P.y (lifted, even Y) (Fp)
328 ForeignFieldLimbs e; // challenge scalar (Fr)
329
330 // ── canonical byte encodings (big-endian) ─────────────────────────
331 std::array<std::uint8_t, 32> bytes_r_y;
332 std::array<std::uint8_t, 32> bytes_pub_y;
333 std::array<std::uint8_t, 32> bytes_e;
334
335 // ── verdict ───────────────────────────────────────────────────────
336 bool valid; // true iff the BIP-340 signature is valid
337};
338
339// Compute the BIP340 Schnorr-in-SNARK foreign-field witness.
340//
341// msg : 32-byte message (per BIP-340, this is the message, not a hash)
342// pubkey_x : 32-byte x-only public key (big-endian)
343// sig_r : 32-byte R.x from signature (big-endian)
344// sig_s : s scalar from signature
345//
346// Returns a fully populated SchnorrSnarkWitness.
347// If the signature is invalid, `valid` is false but witness values are still
348// populated (to allow the prover to build a failing-path proof if needed).
350 const std::array<std::uint8_t, 32>& msg,
351 const std::array<std::uint8_t, 32>& pubkey_x,
352 const std::array<std::uint8_t, 32>& sig_r,
353 const fast::Scalar& sig_s);
354
355} // namespace zk
356} // namespace secp256k1
357
358#endif // SECP256K1_ZK_HPP
void batch_commit(const fast::Scalar *values, const fast::Scalar *blindings, PedersenCommitment *commitments_out, std::size_t count)
const GeneratorVectors & get_generator_vectors()
static constexpr std::size_t RANGE_PROOF_LOG2
Definition zk.hpp:142
RangeProof range_prove(std::uint64_t value, const fast::Scalar &blinding, const PedersenCommitment &commitment, const std::array< std::uint8_t, 32 > &aux_rand)
bool range_verify(const PedersenCommitment &commitment, const RangeProof &proof)
DLEQProof dleq_prove(const fast::Scalar &secret, const fast::Point &G, const fast::Point &H, const fast::Point &P, const fast::Point &Q, const std::array< std::uint8_t, 32 > &aux_rand)
SchnorrSnarkWitness schnorr_snark_witness(const std::array< std::uint8_t, 32 > &msg, const std::array< std::uint8_t, 32 > &pubkey_x, const std::array< std::uint8_t, 32 > &sig_r, const fast::Scalar &sig_s)
EcdsaSnarkWitness ecdsa_snark_witness(const std::array< std::uint8_t, 32 > &msg_hash, const fast::Point &pubkey, const fast::Scalar &sig_r, const fast::Scalar &sig_s)
bool batch_range_verify(const PedersenCommitment *commitments, const RangeProof *proofs, std::size_t count)
bool dleq_verify(const DLEQProof &proof, const fast::Point &G, const fast::Point &H, const fast::Point &P, const fast::Point &Q)
KnowledgeProof knowledge_prove(const fast::Scalar &secret, const fast::Point &pubkey, const std::array< std::uint8_t, 32 > &msg, const std::array< std::uint8_t, 32 > &aux_rand)
static constexpr std::size_t RANGE_PROOF_BITS
Definition zk.hpp:141
bool knowledge_verify_base(const KnowledgeProof &proof, const fast::Point &point, const fast::Point &base, const std::array< std::uint8_t, 32 > &msg)
KnowledgeProof knowledge_prove_base(const fast::Scalar &secret, const fast::Point &point, const fast::Point &base, const std::array< std::uint8_t, 32 > &msg, const std::array< std::uint8_t, 32 > &aux_rand)
bool knowledge_verify(const KnowledgeProof &proof, const fast::Point &pubkey, const std::array< std::uint8_t, 32 > &msg)
fast::Scalar s
Definition zk.hpp:102
fast::Scalar e
Definition zk.hpp:101
std::array< std::uint8_t, 64 > serialize() const
static bool deserialize(const std::uint8_t *data64, DLEQProof &out)
ForeignFieldLimbs u1
Definition zk.hpp:264
ForeignFieldLimbs u2
Definition zk.hpp:265
std::array< std::uint8_t, 32 > bytes_result_x
Definition zk.hpp:274
ForeignFieldLimbs pub_x
Definition zk.hpp:259
std::array< std::uint8_t, 32 > bytes_result_x_mod_n
Definition zk.hpp:276
ForeignFieldLimbs sig_r
Definition zk.hpp:257
ForeignFieldLimbs s_inv
Definition zk.hpp:263
ForeignFieldLimbs result_x
Definition zk.hpp:266
std::array< std::uint8_t, 32 > bytes_u2
Definition zk.hpp:273
std::array< std::uint8_t, 32 > bytes_u1
Definition zk.hpp:272
ForeignFieldLimbs result_x_mod_n
Definition zk.hpp:268
std::array< std::uint8_t, 32 > bytes_result_y
Definition zk.hpp:275
ForeignFieldLimbs result_y
Definition zk.hpp:267
ForeignFieldLimbs sig_s
Definition zk.hpp:258
ForeignFieldLimbs pub_y
Definition zk.hpp:260
ForeignFieldLimbs msg
Definition zk.hpp:256
std::array< std::uint8_t, 32 > bytes_s_inv
Definition zk.hpp:271
std::uint64_t limbs[5]
Definition zk.hpp:240
std::array< fast::Point, RANGE_PROOF_BITS > H
Definition zk.hpp:191
std::array< fast::Point, RANGE_PROOF_BITS > G
Definition zk.hpp:190
std::array< std::uint8_t, 64 > serialize() const
std::array< std::uint8_t, 32 > rx
Definition zk.hpp:52
static bool deserialize(const std::uint8_t *data64, KnowledgeProof &out)
fast::Scalar tau_x
Definition zk.hpp:154
std::array< fast::Point, RANGE_PROOF_LOG2 > R
Definition zk.hpp:160
fast::Scalar mu
Definition zk.hpp:155
fast::Scalar t_hat
Definition zk.hpp:156
std::array< fast::Point, RANGE_PROOF_LOG2 > L
Definition zk.hpp:159
ForeignFieldLimbs pub_x
Definition zk.hpp:323
std::array< std::uint8_t, 32 > bytes_pub_y
Definition zk.hpp:332
std::array< std::uint8_t, 32 > bytes_e
Definition zk.hpp:333
ForeignFieldLimbs pub_y
Definition zk.hpp:327
ForeignFieldLimbs sig_s
Definition zk.hpp:322
ForeignFieldLimbs sig_r
Definition zk.hpp:321
std::array< std::uint8_t, 32 > bytes_r_y
Definition zk.hpp:331