Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BIP374: Discrete Log Equality Proofs (DLEQ) #1689

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4f5d87a
Bip Draft: DLEQ
andrewtoth Oct 24, 2024
0c7e54d
BIP-DLEQ: add reference implementation for secp256k1
theStack Nov 18, 2024
cc7bb12
Add optional message to DLEQ
andrewtoth Dec 9, 2024
ed98dc7
Add some more commentary
andrewtoth Dec 9, 2024
b5d47df
add theStack as co-author
andrewtoth Dec 9, 2024
597004a
Lowercase secp
andrewtoth Dec 11, 2024
e4f1d7b
Remove cbytes wrapper from m'
andrewtoth Dec 11, 2024
b838696
Remove cbytes wrapper from m'
andrewtoth Dec 11, 2024
dab5571
bugfix: respect message m in DLEQ proof generation/verification
theStack Dec 21, 2024
6b16952
Add test vectors for DLEQ proof generation/verification
theStack Dec 20, 2024
1f875a3
Add note about generating and running test vectors
andrewtoth Dec 21, 2024
687198d
Fail if any point is infinity when verifying
andrewtoth Dec 21, 2024
f5d1c12
Add acknowledgements
andrewtoth Dec 21, 2024
fd60d8e
Add description of proof
andrewtoth Dec 21, 2024
90e7027
Remove changelog
andrewtoth Dec 21, 2024
0b590d0
Add footnote recommending using fresh randomness for each proof
andrewtoth Dec 21, 2024
a0d8aad
Fix typo
andrewtoth Dec 21, 2024
5799659
Update bip-DLEQ.mediawiki
andrewtoth Dec 26, 2024
b533b92
Update bip-DLEQ.mediawiki
andrewtoth Dec 26, 2024
1350bc4
BIP374
andrewtoth Dec 26, 2024
9d6dc6b
Update README table, post-history, and comments-uri
andrewtoth Dec 26, 2024
1842120
Clarify restraints on given points
andrewtoth Dec 26, 2024
cb3afee
Move test vectors to bip-0374 directory, add tests for G
andrewtoth Dec 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.mediawiki
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,13 @@ Those proposing changes should consider that ultimately consent may rest with th
| Standard
| Draft
|-
| [[bip-0374.mediawiki|374]]
| Applications
| Discrete Log Equality Proofs
| Andrew Toth, Ruben Somsen, Sebastian Falbesoner
| Standard
| Draft
|-
| [[bip-0379.md|379]]
| Applications
| Miniscript
Expand Down
128 changes: 128 additions & 0 deletions bip-0374.mediawiki
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<pre>
BIP: 374
Layer: Applications
Title: Discrete Log Equality Proofs
Author: Andrew Toth <andrewstoth@gmail.com>
Ruben Somsen <rsomsen@gmail.com>
Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP-0374
Status: Draft
Type: Standards Track
License: BSD-2-Clause
Created: 2024-12-26
Post-History: https://gist.github.com/andrewtoth/df97c3260cc8d12f09d3855ee61322ea
https://groups.google.com/g/bitcoindev/c/MezoKV5md7s
</pre>

== Introduction ==

=== Abstract ===

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 on ly 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''.

=== Copyright ===

This document is licensed under the 2-clause BSD license.

=== Motivation ===

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

== Specification ==

All conventions and notations are used as defined in [https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki#user-content-Notation BIP327].

=== Description ===

The basic proof generation uses a random scalar ''k'', the secret ''a'', and the point being proven ''C = a⋅B''.

* Let ''R<sub>1</sub> = k⋅G''.
* Let ''R<sub>2</sub> = k⋅B''.
* Let ''e = hash(R<sub>1</sub> || R<sub>2</sub>)''.
* Let ''s = (k + e⋅a)''.

Providing only ''C'', ''e'' and ''s'' as a proof does not reveal ''a'' or ''k''.

Verifying the proof involves recreating ''R<sub>1</sub>'' and ''R<sub>2</sub>'' with only ''e'' and ''s'' as follows:

* Let ''R<sub>1</sub> = s⋅G - e⋅A''.
* Let ''R<sub>2</sub> = s⋅B - e⋅C''.

This can be verified by substituting ''s = (k + e⋅a)'':

* ''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''.
* ''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''.

Thus verifying ''e = hash(R<sub>1</sub> || R<sub>2</sub>)'' proves the discrete logarithm equivalency of ''A'' and ''C''.

=== DLEQ Proof Generation ===

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

Input:
* The secret key ''a'': a 256-bit unsigned integer
* The public key ''B'': a point on the curve
* 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>
* 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>
* 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>

The algorithm ''GenerateProof(a, B, r, G, m)'' is defined as:
* Fail if ''a = 0'' or ''a &ge; n''.
* Fail if ''is_infinite(B)''.
* Let ''A = a⋅G''.
* Let ''C = a⋅B''.
* Let ''t'' be the byte-wise xor of ''bytes(32, a)'' and ''hash<sub>BIP0374/aux</sub>(r)''.
* Let ''rand = hash<sub>BIP0374/nonce</sub>(t || cbytes(A) || cbytes(C))''.
* Let ''k = int(rand) mod n''.
* Fail if ''k = 0''.
* Let ''R<sub>1</sub> = k⋅G''.
* Let ''R<sub>2</sub> = k⋅B''.
* Let ''m' = m if m is provided, otherwise an empty byte array''.
* 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'))''.
* Let ''s = (k + e⋅a) mod n''.
* Let ''proof = bytes(32, e) || bytes(32, s)''.
* If ''VerifyProof(A, B, C, proof)'' (see below) returns failure, abort.
* Return the proof ''proof''.

=== DLEQ Proof Verification ===

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

Input:
* The public key of the secret key used in the proof generation ''A'': a point on the curve
* The public key used in the proof generation ''B'': a point on the curve
* The result of multiplying the secret and public keys used in the proof generation ''C'': a point on the curve
* A proof ''proof'': a 64-byte array
* 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>
* 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>

The algorithm ''VerifyProof(A, B, C, proof, G, m)'' is defined as:
* Fail if any of ''is_infinite(A)'', ''is_infinite(B)'', ''is_infinite(C)'', ''is_infinite(G)''
* Let ''e = int(proof[0:32])''.
* Let ''s = int(proof[32:64])''; fail if ''s &ge; n''.
* Let ''R<sub>1</sub> = s⋅G - e⋅A''.
* Fail if ''is_infinite(R<sub>1</sub>)''.
* Let ''R<sub>2</sub> = s⋅B - e⋅C''.
* Fail if ''is_infinite(R<sub>2</sub>)''.
* Let ''m' = m if m is provided, otherwise an empty byte array''.
* 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'))''.
* Return success iff no failure occurred before reaching this point.

==Backwards Compatibility==

This proposal is compatible with all older clients.

== Test Vectors and Reference Code ==

A reference python implementation is included [./bip-0374/reference.py here].
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`.

== Footnotes ==

<references />

== Acknowledgements ==

Thanks to josibake, Tim Ruffing, benma, stratospher, waxwing, Yuval Kogman and all others who
participated in discussions on this topic.
126 changes: 126 additions & 0 deletions bip-0374/gen_test_vectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Generate the BIP-DLEQ test vectors (limited to secp256k1 generator right now)."""
import csv
import os
import sys
from reference import (
TaggedHash,
dleq_generate_proof,
dleq_verify_proof,
)
from secp256k1 import G as GENERATOR, GE


NUM_SUCCESS_TEST_VECTORS = 5
DLEQ_TAG_TESTVECTORS_RNG = "BIP0374/testvectors_rng"

FILENAME_GENERATE_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_generate_proof.csv')
FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv')


def random_scalar_int(vector_i, purpose):
rng_out = TaggedHash(DLEQ_TAG_TESTVECTORS_RNG, purpose.encode() + vector_i.to_bytes(4, 'little'))
return int.from_bytes(rng_out, 'big') % GE.ORDER


def random_bytes(vector_i, purpose):
rng_out = TaggedHash(DLEQ_TAG_TESTVECTORS_RNG, purpose.encode() + vector_i.to_bytes(4, 'little'))
return rng_out


def create_test_vector_data(vector_i):
g = random_scalar_int(vector_i, "scalar_g")
assert g < GE.ORDER
assert g > 0
G = g * GENERATOR
assert not G.infinity
a = random_scalar_int(vector_i, "scalar_a")
A = a * G
b = random_scalar_int(vector_i, "scalar_b")
B = b * G
C = a * B # shared secret
assert C.to_bytes_compressed() == (b * A).to_bytes_compressed()
auxrand = random_bytes(vector_i, "auxrand")
msg = random_bytes(vector_i, "message")
proof = dleq_generate_proof(a, B, auxrand, G=G, m=msg)
return (G, a, A, b, B, C, auxrand, msg, proof)

TEST_VECTOR_DATA = [create_test_vector_data(i) for i in range(NUM_SUCCESS_TEST_VECTORS)]


def gen_all_generate_proof_vectors(f):
writer = csv.writer(f)
writer.writerow(("index", "point_G", "scalar_a", "point_B", "auxrand_r", "message", "result_proof", "comment"))

# success cases with random values
idx = 0
for i in range(NUM_SUCCESS_TEST_VECTORS):
G, a, A, b, B, C, auxrand, msg, proof = TEST_VECTOR_DATA[i]
assert proof is not None and len(proof) == 64
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}"))
idx += 1

# failure cases: a is not within group order (a=0, a=N)
a_invalid = 0
assert dleq_generate_proof(a_invalid, B, auxrand, G=G, m=msg) is None
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)"))
idx += 1
a_invalid = GE.ORDER
assert dleq_generate_proof(a_invalid, B, auxrand, G=G, m=msg) is None
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])"))
idx += 1

# failure case: B is point at infinity
B_infinity = GE()
B_infinity_str = "INFINITY"
assert dleq_generate_proof(a, B_infinity, auxrand, m=msg) is None
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)"))
idx += 1


def gen_all_verify_proof_vectors(f):
writer = csv.writer(f)
writer.writerow(("index", "point_G", "point_A", "point_B", "point_C", "proof", "message", "result_success", "comment"))

# success cases (same as above)
idx = 0
for i in range(NUM_SUCCESS_TEST_VECTORS):
G, _, A, _, B, C, _, msg, proof = TEST_VECTOR_DATA[i]
assert dleq_verify_proof(A, B, C, proof, G=G, m=msg)
writer.writerow((idx, G.to_bytes_compressed().hex(), A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(),
C.to_bytes_compressed().hex(), proof.hex(), msg.hex(), "TRUE", f"Success case {i+1}"))
idx += 1

# other permutations of A, B, C should always fail
for i, points in enumerate(([A, C, B], [B, A, C], [B, C, A], [C, A, B], [C, B, A])):
assert not dleq_verify_proof(points[0], points[1], points[2], proof, m=msg)
writer.writerow((idx, G.to_bytes_compressed().hex(), points[0].to_bytes_compressed().hex(), points[1].to_bytes_compressed().hex(),
points[2].to_bytes_compressed().hex(), proof.hex(), msg.hex(), "FALSE", f"Swapped points case {i+1}"))
idx += 1

# modifying proof should fail (flip one bit)
proof_damage_pos = random_scalar_int(idx, "damage_pos") % 256
proof_damaged = list(proof)
proof_damaged[proof_damage_pos // 8] ^= (1 << (proof_damage_pos % 8))
proof_damaged = bytes(proof_damaged)
writer.writerow((idx, G.to_bytes_compressed().hex(), A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(),
C.to_bytes_compressed().hex(), proof_damaged.hex(), msg.hex(), "FALSE", f"Tampered proof (random bit-flip)"))
idx += 1

# modifying message should fail (flip one bit)
msg_damage_pos = random_scalar_int(idx, "damage_pos") % 256
msg_damaged = list(msg)
msg_damaged[proof_damage_pos // 8] ^= (1 << (msg_damage_pos % 8))
msg_damaged = bytes(msg_damaged)
writer.writerow((idx, G.to_bytes_compressed().hex(), A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(),
C.to_bytes_compressed().hex(), proof.hex(), msg_damaged.hex(), "FALSE", f"Tampered message (random bit-flip)"))
idx += 1


if __name__ == "__main__":
print(f"Generating {FILENAME_GENERATE_PROOF_TEST}...")
with open(FILENAME_GENERATE_PROOF_TEST, "w", encoding="utf-8") as fil_generate_proof:
gen_all_generate_proof_vectors(fil_generate_proof)
print(f"Generating {FILENAME_VERIFY_PROOF_TEST}...")
with open(FILENAME_VERIFY_PROOF_TEST, "w", encoding="utf-8") as fil_verify_proof:
gen_all_verify_proof_vectors(fil_verify_proof)
Loading
Loading