Skip to content

Add DLEQ proof implementing BIP 374#1802

Open
macgyver13 wants to merge 6 commits intobitcoin-core:masterfrom
macgyver13:dleq-module-standalone
Open

Add DLEQ proof implementing BIP 374#1802
macgyver13 wants to merge 6 commits intobitcoin-core:masterfrom
macgyver13:dleq-module-standalone

Conversation

@macgyver13
Copy link
Copy Markdown

@macgyver13 macgyver13 commented Jan 15, 2026

This PR adds a DLEQ (Discrete Logarithm Equality) proof module as specified in BIP 374.

Based on PR #1651 by @stratospher and the secp256k1-zkp implementation.

Public API

Exposes two functions for proof generation and verification:

  • secp256k1_dleq_prove
  • secp256k1_dleq_verify

These are designed to support rust FFI bindings and follow the API patterns established in similar modules.

Questions for Reviewers

  • Should this module be optional (current behavior) or enabled by default?
  • Are there additional changes needed to support existing Silent Payments PRs?
  • Feedback on API design, documentation, and test coverage?

Notes

Addressed outstanding comments from PR #1651:

  • BIP-374 v0.2.0 test vectors
  • Proper memory clearing with memclear

@furszy
Copy link
Copy Markdown
Member

furszy commented Jan 15, 2026

Have you talked with @stratospher before opening this PR?
I don’t want to draw conclusions prematurely, but opening a parallel PR that implements the same changes without first engaging on the existing one by reviewing it, leaving a comment, or waiting for the author's public response is not how we usually collaborate in open source. The lack of (co)authorship on the commits is also unusual.

If you have contacted stratospher, please ignore this comment. It's just to avoid putting the original author in an unfair or difficult position if still working on the PR.

@macgyver13
Copy link
Copy Markdown
Author

Have you talked with @stratospher before opening this PR?

Yes, stratospher and I had discussed who should submit this new PR. I drew the short straw :)

@macgyver13 macgyver13 marked this pull request as draft January 15, 2026 10:55
@macgyver13 macgyver13 force-pushed the dleq-module-standalone branch from 3c538a6 to 30d6f5e Compare January 15, 2026 11:07
@macgyver13 macgyver13 marked this pull request as ready for review January 15, 2026 12:02
Copy link
Copy Markdown
Contributor

@stratospher stratospher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry for the confusion and thank you @macgyver13 for the PR!

did an initial pass and it looks good! mostly left style nits.

1 API design question I had is whether it would be better for the proof generation API to also accept C as an argument - that is GenerateProof(a, B, r, G, m, C) kind of API instead of GenerateProof(a, B, r, G, m).

When computing silent payment output points, we anyways compute shared secret/C for each output - so there's a possibility to avoid recomputation again if we can pass C. But current approach might be safer since callers could accidentally provide incorrect C and errors could happen.

Did you check other use cases of DLEQ for whether they might prefer passing C or computing C internally in the proof generation function?

Comment thread src/modules/dleq/main_impl.h Outdated
static int secp256k1_dleq_hash_point(secp256k1_sha256 *sha, secp256k1_ge *p) {
unsigned char buf[33];
size_t size = 33;
/* Reject infinity point */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2457f89: I guess this comment might be applicable now since the public API takes in secp256k1_pubkey and the ge we get is now from there - so unlikely for it to be infinity and VERIFY_CHECK might be better. though if we think about a function in isolation - it makes sense to reject infinity point.

cc @theStack

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

45911b2 attempts to handle the 'invalid proof (e.g., all zeros)' case more clearly by moving the infinity check from secp256k1_dleq_hash_point to secp256k1_dleq_verify_internal, where R1 and R2 are computed. The check now occurs before calling secp256k1_dleq_challenge.

A check is required: invalid proofs (e.g., all zeros) will generate R1 and R2 as infinity points, which would cause verification to fail at VERIFY_CHECK(!secp256k1_ge_is_infinity(elem)); during serialization.

Open to a better solution.

Comment thread src/modules/dleq/main_impl.h Outdated
}

secp256k1_nonce_function_bip374_sha256_tagged(&sha);
/* Hash masked-key||msg||m using the tagged hash as per BIP-374 v0.2.0 */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2457f89: you could clarify that msg contains A and C. and that's different from m.

Comment thread src/modules/dleq/main_impl.h Outdated
secp256k1_memclear_explicit(masked_key, sizeof(masked_key));
}

/* Generates a nonce as defined in BIP0374 v0.2.0 */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2457f89: not sure if we should mention the exact version - we don't mention the version for other modules like musig for example. only difference in newer version is the randomness computation in proof generation anyways.

Comment thread src/modules/dleq/main_impl.h Outdated

ret = secp256k1_dleq_prove_internal(ctx, &s, &e, &a, &B, &A, &C, aux_rand32, msg);
if (!ret) {
secp256k1_scalar_clear(&a);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

078d2da: since we always clear a - we can move it up and out of the if condition.

Comment thread src/modules/dleq/tests_impl.h Outdated
if (i > 2 && i < 6) {
/* Skip tests indices 3-5: proof generation failure cases (a=0, a=N, B=infinity).
* These contain placeholder data from test_vectors_generate_proof.csv that would
* fail to parse. Only indices 0-2 and 6-12 have valid test data.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: 2614b2a: extra white space

Comment thread src/modules/dleq/tests_impl.h Outdated
CHECK_ILLEGAL(CTX, secp256k1_dleq_verify(CTX, proof, NULL, &B, &C, msg));
CHECK_ILLEGAL(CTX, secp256k1_dleq_verify(CTX, proof, &A, NULL, &C, msg));
CHECK_ILLEGAL(CTX, secp256k1_dleq_verify(CTX, proof, &A, &B, NULL, msg));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: 9149b57: extra white space in blank line

Comment thread src/modules/dleq/Makefile.am.include Outdated
@@ -0,0 +1,3 @@
include_HEADERS += include/secp256k1_dleq.h
noinst_HEADERS += src/modules/dleq/dleq_vectors.h
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2614b2a: tests also need to be added. nit: slight ordering preference for main_impl before test vectors - you can refer some other makefile.

Comment thread tools/test_vectors_dleq_generate.py Outdated
for _ in range(5): # Skip the first 5 rows since those test vectors don't use secp's generator point
next(reader, None)

for i in range(3):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: 2614b2a: extra white space


#include "../../../include/secp256k1.h"
#include "../../../include/secp256k1_dleq.h"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2457f89: #include "../../hash.h"

#ifndef SECP256K1_MODULE_DLEQ_TESTS_H
#define SECP256K1_MODULE_DLEQ_TESTS_H

#include "dleq_vectors.h"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2614b2a: #include "../../unit_test.h"

@qatkk
Copy link
Copy Markdown

qatkk commented Jan 18, 2026

I have a question regarding the choice of parameters passed to this API, and I may be missing some design context here — so I’d appreciate clarification.
Is there a specific reason why the second generator B is provided directly as an input? @furszy
In the original proposal of DLEQ proofs by Chaum and Pedersen [1], the correctness of the protocol relies on the prover not knowing the discrete‑log relation between the two generators. Specifically, if the prover knows a scalar b such that B=b⋅G, then the two equations for the verification are no longer distinct and will be dependent on each other.
In the context of silent payments (and the description provided in BIP 374) this makes sense, since B represents another user’s public key — a point for which the prover does not know the corresponding private key. However, as commented by stratospher #1651 (comment) this PR is intended to support more general DLEQ use cases, I am concerned that allowing callers to supply an arbitrary B might introduce risks in scenarios where generator independence is not guaranteed.

Other constructions with two generators on the same curve (e.g., Pedersen commitments, bulletproofs) typically derive the second generator using a hash‑to‑curve procedure, ensuring that no participant knows the discrete‑log ratio between G and B. I was wondering whether a similar approach might help avoid potential misuse here, or whether this falls outside the intended scope of this PR.
I would be very interested to hear your thoughts on this.
[1] D. Chaum and T. P. Pedersen, Wallet Databases with Observers, 1992.
https://www.hsslb.ch/cryptopapers/other/Chaum_WalletDBswObservers.pdf

macgyver13 and others added 6 commits January 25, 2026 14:30
Implements BIP-374 Discrete Log Equality (DLEQ) proofs as a new module.

- Build system integration (CMake, autotools)
- Configure dleq module as "optional"
- Public API header declarations
- Internal cryptographic implementation (prove_internal, verify_internal)
- Tagged SHA256 functions per BIP-374 specification
- Nonce generation following BIP-374 v0.2.0

Co-authored-by: stratospher <44024636+stratospher@users.noreply.github.com>
- Adds test coverage for DLEQ internal functions
- BIP-374 official test vectors (6 generation + 13 verification cases)
- Includes Python script for generating test vectors matching BIP-374
specification.

Tests call *_internal functions directly. Public API tests will be added in a subsequent commit.

Co-authored-by: stratospher <44024636+stratospher@users.noreply.github.com>
Adds public API functions that wrap the internal DLEQ implementation:

- secp256k1_dleq_prove(): Generate DLEQ proof from secret key and base point B.
  Computes A = a*G and C = a*B internally, then generates proof.

- secp256k1_dleq_verify(): Verify DLEQ proof given A, B, C public keys.
Adds comprehensive tests for the public DLEQ API:

- secp256k1_dleq_prove(): Tests valid proof generation, NULL parameter
  detection, invalid secret key handling, context validation

- secp256k1_dleq_verify(): Tests valid verification, NULL parameter
  detection for all required inputs (proof, A, B, C)
Reject proofs that produce infinity points during verification
Add public API test case for infinity point scenario
Add note to clarify contents of msg to reduce confusion with m
Add missing includes
Format tools/test_vectors_dleq_generate.py
@macgyver13 macgyver13 force-pushed the dleq-module-standalone branch from 30d6f5e to 45911b2 Compare January 25, 2026 20:04
@real-or-random
Copy link
Copy Markdown
Contributor

In the original proposal of DLEQ proofs by Chaum and Pedersen [1], the correctness of the protocol relies on the prover not knowing the discrete‑log relation between the two generators.

You probably mean soundness instead of correctness? If I'm not mistaken, this is simply not true. See for instance Nigel Smart's Cryptography: An Introduction (3rd Edition), Section 25.3.1, page 377, PDF page 389. This rather accessible write-up shows that the underlying Sigma protocol has correctness, 2-special soundness, and zero-knowledge, all with probability 1 and without any computational assumption. Technically, one doesn't even need the assumption that the discrete logarithm problem is hard in the group. Note in particular that the proof of special soundness extracts the witness and not the discrete logarithm between g and h (or G and B in our notation); the latter doesn't even show up in the proof.

Specifically, if the prover knows a scalar b such that B=b⋅G, then the two equations for the verification are no longer distinct and will be dependent on each other.

Can you elaborate and also explain why you believe that this requires the assumption that the prover doesn't know the discrete logarithm between G and B?

@macgyver13
Copy link
Copy Markdown
Author

Thank you for the review @stratospher and appreciate the curious questions about the API!

1 API design question I had is whether it would be better for the proof generation API to also accept C as an argument - that is GenerateProof(a, B, r, G, m, C) kind of API instead of GenerateProof(a, B, r, G, m).

When computing silent payment output points, we anyways compute shared secret/C for each output - so there's a possibility to avoid recomputation again if we can pass C. But current approach might be safer since callers could accidentally provide incorrect C and errors could happen.

If we accept C as an input, I would suggest computing C internally to verify correctness before calling secp256k1_dleq_prove_internal. This would counter the optimization benefit. This is the approach I took when testing that variation of the API.

Did you check other use cases of DLEQ for whether they might prefer passing C or computing C internally in the proof generation function?

I am not well versed in other DLEQ use cases outside Silent Payments, so I surveyed related applications of Discrete Log Equivalence starting with Andrew Toth's BIP-374 review on the OpTech Podcast. Below you will find a list of reviewed use cases and my assessment of whether to require output points like C as input parameters to proof generation.

DLEQ implementations reviewed:

Related protocols reviewed (not DLEQ-based):

  • Curve Trees - anonymous usage tokens from curve trees
  • RIDDLE - ring signatures (LSAG) with key images

I don't see a compelling reason to change the API based on these findings, but I'm open to different perspectives.

@qatkk
Copy link
Copy Markdown

qatkk commented Feb 10, 2026

Thanks for your response and for the explanation.

You probably mean soundness instead of correctness?

Yes — thanks for the correction.

If I'm not mistaken, this is simply not true. See for instance Nigel Smart's Cryptography: An Introduction (3rd Edition), Section 25.3.1…

Agreed. I’ve encountered DLEQ mostly in other contexts, and in particular in scenarios involving DLEQ across two different curves. In those settings, Pedersen commitments are typically used, which require the independence of the two generators.
Writing out the equations explicitly for the single-curve setting makes it clear that this independence assumption is not needed here, as you point out.

Can you elaborate and also explain why you believe that this requires the assumption that the prover doesn't know the discrete logarithm between G and B?

My reasoning was that the second verification equation (the one checking R₂ in this notation) is effectively just the first verification equation multiplied by b, due to how the parameters are constructed. This made me think that knowing the discrete logarithm between G and B might collapse the two checks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants