Skip to content

Commit 75b12ac

Browse files
authored
Merge pull request #1689 from andrewtoth/dleq
BIP374: Discrete Log Equality Proofs (DLEQ)
2 parents b509e6c + 248540e commit 75b12ac

File tree

6 files changed

+836
-0
lines changed

6 files changed

+836
-0
lines changed

README.mediawiki

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,13 @@ Those proposing changes should consider that ultimately consent may rest with th
11911191
| Standard
11921192
| Draft
11931193
|-
1194+
| [[bip-0374.mediawiki|374]]
1195+
| Applications
1196+
| Discrete Log Equality Proofs
1197+
| Andrew Toth, Ruben Somsen, Sebastian Falbesoner
1198+
| Standard
1199+
| Draft
1200+
|-
11941201
| [[bip-0379.md|379]]
11951202
| Applications
11961203
| Miniscript

bip-0374.mediawiki

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<pre>
2+
BIP: 374
3+
Layer: Applications
4+
Title: Discrete Log Equality Proofs
5+
Author: Andrew Toth <[email protected]>
6+
Ruben Somsen <[email protected]>
7+
Sebastian Falbesoner <[email protected]>
8+
Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP-0374
9+
Status: Draft
10+
Type: Standards Track
11+
License: BSD-2-Clause
12+
Created: 2024-12-26
13+
Post-History: https://gist.github.com/andrewtoth/df97c3260cc8d12f09d3855ee61322ea
14+
https://groups.google.com/g/bitcoindev/c/MezoKV5md7s
15+
</pre>
16+
17+
== Introduction ==
18+
19+
=== Abstract ===
20+
21+
This document proposes a standard for 64-byte zero-knowledge ''discrete logarithm equality proofs'' (DLEQ proofs) over an elliptic curve. For given elliptic curve points ''A'', ''B'', ''C'', ''G'', and a scalar ''a'' known only to the prover where ''A = a⋅G'' and ''C = a⋅B'', the prover proves knowledge of ''a'' without revealing anything about ''a''. This can, for instance, be useful in ECDH: if ''A'' and ''B'' are ECDH public keys, and ''C'' is their ECDH shared secret computed as ''C = a⋅B'', the proof establishes that the same secret key ''a'' is used for generating both ''A'' and ''C'' without revealing ''a''.
22+
23+
=== Copyright ===
24+
25+
This document is licensed under the 2-clause BSD license.
26+
27+
=== Motivation ===
28+
29+
[https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#specification BIP352] requires senders to compute output scripts using ECDH shared secrets from the same secret keys used to sign the inputs. Generating an incorrect signature will produce an invalid transaction that will be rejected by consensus. An incorrectly generated output script can still be consensus-valid, meaning funds may be lost if it gets broadcast.
30+
By producing a DLEQ proof for the generated ECDH shared secrets, the signing entity can prove to other entities that the output scripts have been generated correctly without revealing the private keys.
31+
32+
== Specification ==
33+
34+
All conventions and notations are used as defined in [https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki#user-content-Notation BIP327].
35+
36+
=== Description ===
37+
38+
The basic proof generation uses a random scalar ''k'', the secret ''a'', and the point being proven ''C = a⋅B''.
39+
40+
* Let ''R<sub>1</sub> = k⋅G''.
41+
* Let ''R<sub>2</sub> = k⋅B''.
42+
* Let ''e = hash(R<sub>1</sub> || R<sub>2</sub>)''.
43+
* Let ''s = (k + e⋅a)''.
44+
45+
Providing only ''C'', ''e'' and ''s'' as a proof does not reveal ''a'' or ''k''.
46+
47+
Verifying the proof involves recreating ''R<sub>1</sub>'' and ''R<sub>2</sub>'' with only ''e'' and ''s'' as follows:
48+
49+
* Let ''R<sub>1</sub> = s⋅G - e⋅A''.
50+
* Let ''R<sub>2</sub> = s⋅B - e⋅C''.
51+
52+
This can be verified by substituting ''s = (k + e⋅a)'':
53+
54+
* ''s⋅G - e⋅A = (k + e⋅a)⋅G - e⋅A = k⋅G + e⋅(a⋅G) - e⋅A = k⋅G + e⋅A - e⋅A = k⋅G''.
55+
* ''s⋅B - e⋅C = (k + e⋅a)⋅B - e⋅C = k⋅B + e⋅(a⋅B) - e⋅C = k⋅B + e⋅C - e⋅C = k⋅B''.
56+
57+
Thus verifying ''e = hash(R<sub>1</sub> || R<sub>2</sub>)'' proves the discrete logarithm equivalency of ''A'' and ''C''.
58+
59+
=== DLEQ Proof Generation ===
60+
61+
The following generates a proof that the result of ''a⋅B'' and the result of ''a⋅G'' are both generated from the same scalar ''a'' without having to reveal ''a''.
62+
63+
Input:
64+
* The secret key ''a'': a 256-bit unsigned integer
65+
* The public key ''B'': a point on the curve
66+
* Auxiliary random data ''r'': a 32-byte array<ref name="why_include_auxiliary_random_data"> ''' Why include auxiliary random data?''' The auxiliary random data should be set to fresh randomness for each proof. The same rationale and recommendations from [https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#default-signing BIP340] should be applied.</ref>
67+
* The generator point ''G'': a point on the curve<ref name="why_include_G"> ''' Why include the generator point G as an input?''' While all other BIPs have used the generator point from secp256k1, passing it as an input here lets this algorithm be used for other curves.</ref>
68+
* An optional message ''m'': a 32-byte array<ref name="why_include_a_message"> ''' Why include a message as an input?''' This could be useful for protocols that want to authorize on a compound statement, not just knowledge of a scalar. This allows the protocol to combine knowledge of the scalar and the statement.</ref>
69+
70+
The algorithm ''GenerateProof(a, B, r, G, m)'' is defined as:
71+
* Fail if ''a = 0'' or ''a &ge; n''.
72+
* Fail if ''is_infinite(B)''.
73+
* Let ''A = a⋅G''.
74+
* Let ''C = a⋅B''.
75+
* Let ''t'' be the byte-wise xor of ''bytes(32, a)'' and ''hash<sub>BIP0374/aux</sub>(r)''.
76+
* Let ''rand = hash<sub>BIP0374/nonce</sub>(t || cbytes(A) || cbytes(C))''.
77+
* Let ''k = int(rand) mod n''.
78+
* Fail if ''k = 0''.
79+
* Let ''R<sub>1</sub> = k⋅G''.
80+
* Let ''R<sub>2</sub> = k⋅B''.
81+
* Let ''m' = m if m is provided, otherwise an empty byte array''.
82+
* Let ''e = int(hash<sub>BIP0374/challenge</sub>(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R<sub>1</sub>) || cbytes(R<sub>2</sub>) || m'))''.
83+
* Let ''s = (k + e⋅a) mod n''.
84+
* Let ''proof = bytes(32, e) || bytes(32, s)''.
85+
* If ''VerifyProof(A, B, C, proof)'' (see below) returns failure, abort.
86+
* Return the proof ''proof''.
87+
88+
=== DLEQ Proof Verification ===
89+
90+
The following verifies the proof generated in the previous section. If the following algorithm succeeds, the points ''A'' and ''C'' were both generated from the same scalar. The former from multiplying by ''G'', and the latter from multiplying by ''B''.
91+
92+
Input:
93+
* The public key of the secret key used in the proof generation ''A'': a point on the curve
94+
* The public key used in the proof generation ''B'': a point on the curve
95+
* The result of multiplying the secret and public keys used in the proof generation ''C'': a point on the curve
96+
* A proof ''proof'': a 64-byte array
97+
* The generator point used in the proof generation ''G'': a point on the curve<ref name="why_include_G"> ''' Why include the generator point G as an input?''' While all other BIPs have used the generator point from Secp256k1, passing it as an input here lets this algorithm be used for other curves.</ref>
98+
* An optional message ''m'': a 32-byte array<ref name="why_include_a_message"> ''' Why include a message as an input?''' This could be useful for protocols that want to authorize on a compound statement, not just knowledge of a scalar. This allows the protocol to combine knowledge of the scalar and the statement.</ref>
99+
100+
The algorithm ''VerifyProof(A, B, C, proof, G, m)'' is defined as:
101+
* Fail if any of ''is_infinite(A)'', ''is_infinite(B)'', ''is_infinite(C)'', ''is_infinite(G)''
102+
* Let ''e = int(proof[0:32])''.
103+
* Let ''s = int(proof[32:64])''; fail if ''s &ge; n''.
104+
* Let ''R<sub>1</sub> = s⋅G - e⋅A''.
105+
* Fail if ''is_infinite(R<sub>1</sub>)''.
106+
* Let ''R<sub>2</sub> = s⋅B - e⋅C''.
107+
* Fail if ''is_infinite(R<sub>2</sub>)''.
108+
* Let ''m' = m if m is provided, otherwise an empty byte array''.
109+
* Fail if ''e ≠ int(hash<sub>BIP0374/challenge</sub>(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R<sub>1</sub>) || cbytes(R<sub>2</sub>) || m'))''.
110+
* Return success iff no failure occurred before reaching this point.
111+
112+
==Backwards Compatibility==
113+
114+
This proposal is compatible with all older clients.
115+
116+
== Test Vectors and Reference Code ==
117+
118+
A reference python implementation is included [./bip-0374/reference.py here].
119+
Test vectors can be generated by running `./bip-0374/gen_test_vectors.py` which will produce a CSV file of random test vectors for both generating and verifying proofs. These can be run against the reference implementation with `./bip-0374/run_test_vectors.py`.
120+
121+
== Footnotes ==
122+
123+
<references />
124+
125+
== Acknowledgements ==
126+
127+
Thanks to josibake, Tim Ruffing, benma, stratospher, waxwing, Yuval Kogman and all others who
128+
participated in discussions on this topic.

bip-0374/gen_test_vectors.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/env python3
2+
"""Generate the BIP-DLEQ test vectors (limited to secp256k1 generator right now)."""
3+
import csv
4+
import os
5+
import sys
6+
from reference import (
7+
TaggedHash,
8+
dleq_generate_proof,
9+
dleq_verify_proof,
10+
)
11+
from secp256k1 import G as GENERATOR, GE
12+
13+
14+
NUM_SUCCESS_TEST_VECTORS = 5
15+
DLEQ_TAG_TESTVECTORS_RNG = "BIP0374/testvectors_rng"
16+
17+
FILENAME_GENERATE_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_generate_proof.csv')
18+
FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv')
19+
20+
21+
def random_scalar_int(vector_i, purpose):
22+
rng_out = TaggedHash(DLEQ_TAG_TESTVECTORS_RNG, purpose.encode() + vector_i.to_bytes(4, 'little'))
23+
return int.from_bytes(rng_out, 'big') % GE.ORDER
24+
25+
26+
def random_bytes(vector_i, purpose):
27+
rng_out = TaggedHash(DLEQ_TAG_TESTVECTORS_RNG, purpose.encode() + vector_i.to_bytes(4, 'little'))
28+
return rng_out
29+
30+
31+
def create_test_vector_data(vector_i):
32+
g = random_scalar_int(vector_i, "scalar_g")
33+
assert g < GE.ORDER
34+
assert g > 0
35+
G = g * GENERATOR
36+
assert not G.infinity
37+
a = random_scalar_int(vector_i, "scalar_a")
38+
A = a * G
39+
b = random_scalar_int(vector_i, "scalar_b")
40+
B = b * G
41+
C = a * B # shared secret
42+
assert C.to_bytes_compressed() == (b * A).to_bytes_compressed()
43+
auxrand = random_bytes(vector_i, "auxrand")
44+
msg = random_bytes(vector_i, "message")
45+
proof = dleq_generate_proof(a, B, auxrand, G=G, m=msg)
46+
return (G, a, A, b, B, C, auxrand, msg, proof)
47+
48+
TEST_VECTOR_DATA = [create_test_vector_data(i) for i in range(NUM_SUCCESS_TEST_VECTORS)]
49+
50+
51+
def gen_all_generate_proof_vectors(f):
52+
writer = csv.writer(f)
53+
writer.writerow(("index", "point_G", "scalar_a", "point_B", "auxrand_r", "message", "result_proof", "comment"))
54+
55+
# success cases with random values
56+
idx = 0
57+
for i in range(NUM_SUCCESS_TEST_VECTORS):
58+
G, a, A, b, B, C, auxrand, msg, proof = TEST_VECTOR_DATA[i]
59+
assert proof is not None and len(proof) == 64
60+
writer.writerow((idx, G.to_bytes_compressed().hex(), f"{a:064x}", B.to_bytes_compressed().hex(), auxrand.hex(), msg.hex(), proof.hex(), f"Success case {i+1}"))
61+
idx += 1
62+
63+
# failure cases: a is not within group order (a=0, a=N)
64+
a_invalid = 0
65+
assert dleq_generate_proof(a_invalid, B, auxrand, G=G, m=msg) is None
66+
writer.writerow((idx, G.to_bytes_compressed().hex(), f"{a_invalid:064x}", B.to_bytes_compressed().hex(), auxrand.hex(), msg.hex(), "INVALID", f"Failure case (a=0)"))
67+
idx += 1
68+
a_invalid = GE.ORDER
69+
assert dleq_generate_proof(a_invalid, B, auxrand, G=G, m=msg) is None
70+
writer.writerow((idx, G.to_bytes_compressed().hex(), f"{a_invalid:064x}", B.to_bytes_compressed().hex(), auxrand.hex(), msg.hex(), "INVALID", f"Failure case (a=N [group order])"))
71+
idx += 1
72+
73+
# failure case: B is point at infinity
74+
B_infinity = GE()
75+
B_infinity_str = "INFINITY"
76+
assert dleq_generate_proof(a, B_infinity, auxrand, m=msg) is None
77+
writer.writerow((idx, G.to_bytes_compressed().hex(), f"{a:064x}", B_infinity_str, auxrand.hex(), msg.hex(), "INVALID", f"Failure case (B is point at infinity)"))
78+
idx += 1
79+
80+
81+
def gen_all_verify_proof_vectors(f):
82+
writer = csv.writer(f)
83+
writer.writerow(("index", "point_G", "point_A", "point_B", "point_C", "proof", "message", "result_success", "comment"))
84+
85+
# success cases (same as above)
86+
idx = 0
87+
for i in range(NUM_SUCCESS_TEST_VECTORS):
88+
G, _, A, _, B, C, _, msg, proof = TEST_VECTOR_DATA[i]
89+
assert dleq_verify_proof(A, B, C, proof, G=G, m=msg)
90+
writer.writerow((idx, G.to_bytes_compressed().hex(), A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(),
91+
C.to_bytes_compressed().hex(), proof.hex(), msg.hex(), "TRUE", f"Success case {i+1}"))
92+
idx += 1
93+
94+
# other permutations of A, B, C should always fail
95+
for i, points in enumerate(([A, C, B], [B, A, C], [B, C, A], [C, A, B], [C, B, A])):
96+
assert not dleq_verify_proof(points[0], points[1], points[2], proof, m=msg)
97+
writer.writerow((idx, G.to_bytes_compressed().hex(), points[0].to_bytes_compressed().hex(), points[1].to_bytes_compressed().hex(),
98+
points[2].to_bytes_compressed().hex(), proof.hex(), msg.hex(), "FALSE", f"Swapped points case {i+1}"))
99+
idx += 1
100+
101+
# modifying proof should fail (flip one bit)
102+
proof_damage_pos = random_scalar_int(idx, "damage_pos") % 256
103+
proof_damaged = list(proof)
104+
proof_damaged[proof_damage_pos // 8] ^= (1 << (proof_damage_pos % 8))
105+
proof_damaged = bytes(proof_damaged)
106+
writer.writerow((idx, G.to_bytes_compressed().hex(), A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(),
107+
C.to_bytes_compressed().hex(), proof_damaged.hex(), msg.hex(), "FALSE", f"Tampered proof (random bit-flip)"))
108+
idx += 1
109+
110+
# modifying message should fail (flip one bit)
111+
msg_damage_pos = random_scalar_int(idx, "damage_pos") % 256
112+
msg_damaged = list(msg)
113+
msg_damaged[proof_damage_pos // 8] ^= (1 << (msg_damage_pos % 8))
114+
msg_damaged = bytes(msg_damaged)
115+
writer.writerow((idx, G.to_bytes_compressed().hex(), A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(),
116+
C.to_bytes_compressed().hex(), proof.hex(), msg_damaged.hex(), "FALSE", f"Tampered message (random bit-flip)"))
117+
idx += 1
118+
119+
120+
if __name__ == "__main__":
121+
print(f"Generating {FILENAME_GENERATE_PROOF_TEST}...")
122+
with open(FILENAME_GENERATE_PROOF_TEST, "w", encoding="utf-8") as fil_generate_proof:
123+
gen_all_generate_proof_vectors(fil_generate_proof)
124+
print(f"Generating {FILENAME_VERIFY_PROOF_TEST}...")
125+
with open(FILENAME_VERIFY_PROOF_TEST, "w", encoding="utf-8") as fil_verify_proof:
126+
gen_all_verify_proof_vectors(fil_verify_proof)

0 commit comments

Comments
 (0)