From 83c1c78bb6241f4f3b945c76ee6e4cc34a4fb850 Mon Sep 17 00:00:00 2001 From: notTanveer Date: Fri, 15 May 2026 17:18:25 +0530 Subject: [PATCH 1/2] feat(BIP-374): dleq proofs --- src/embit/silent_payments/dleq.py | 222 ++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 src/embit/silent_payments/dleq.py diff --git a/src/embit/silent_payments/dleq.py b/src/embit/silent_payments/dleq.py new file mode 100644 index 00000000..be96f249 --- /dev/null +++ b/src/embit/silent_payments/dleq.py @@ -0,0 +1,222 @@ +""" +BIP-374 DLEQ (Discrete Log Equality Proofs). + +Given points A = a*G and C = a*B, proves knowledge of scalar a +across both bases (G and B) without revealing a. + +Spec: https://github.com/bitcoin/bips/blob/master/bip-0374.mediawiki +Reference: https://github.com/bitcoin/bips/blob/master/bip-0374/reference.py +""" + +from .. import hashes +from ..util.key import SECP256K1_ORDER, SECP256K1_G +from ..util.secp256k1 import ( + ec_pubkey_create, + ec_pubkey_parse, + ec_pubkey_serialize, + ec_pubkey_tweak_mul, + ec_pubkey_combine, + EC_COMPRESSED, +) + +_G_COMPRESSED = b"\x02" + SECP256K1_G[0].to_bytes(32, "big") +DLEQ_TAG_AUX = "BIP0374/aux" +DLEQ_TAG_NONCE = "BIP0374/nonce" +DLEQ_TAG_CHALLENGE = "BIP0374/challenge" + + +class DLEQError(Exception): + """Raised when DLEQ proof generation fails due to invalid inputs.""" + + +def _scalar_mult_G(scalar_bytes, G_sec): + """Return scalar * G_sec as an opaque pubkey handle. + + Uses ec_pubkey_create for the standard secp256k1 generator (fast path) + and parse + tweak_mul for an arbitrary base point. + Raises ValueError / OverflowError on invalid inputs. + """ + if G_sec == _G_COMPRESSED: + return ec_pubkey_create(scalar_bytes) + pub = ec_pubkey_parse(G_sec) + ec_pubkey_tweak_mul(pub, scalar_bytes) + return pub + + +def generate_dleq_proof(a_bytes, B_sec, r=None, m=None, G=None): + """ + Generate a 64-byte DLEQ proof (BIP-374 GenerateProof). + + Proves log_G(A) == log_B(C) without revealing scalar a, + where A = a*G and C = a*B. + + Args: + a_bytes: 32-byte private key scalar. + B_sec: 33-byte compressed pubkey (alternative base point B). + r: 32-byte auxiliary randomness. Must be fresh per proof; reusing r + with the same (a, B) and no m can leak a. When m is provided, + it is mixed into nonce generation, so that leak does not occur + if the reused r is paired with different messages, but fresh r + is still required. + G: Optional 33-byte compressed base point. Defaults to secp256k1 G. + BIP-374 footnote 2 allows any curve point as the generator so + the algorithm can be used with alternate bases on secp256k1. + + Returns: + 64-byte proof: bytes(32, e) || bytes(32, s). + + Raises: + DLEQError: on invalid inputs or internal self-verification failure. + """ + if len(a_bytes) != 32: + raise DLEQError("a_bytes must be 32 bytes") + if len(B_sec) != 33: + raise DLEQError("B_sec must be a 33-byte compressed pubkey") + if m is not None and len(m) != 32: + raise DLEQError("m must be exactly 32 bytes per BIP-374") + if G is not None and len(G) != 33: + raise DLEQError("G must be a 33-byte compressed pubkey") + + G_sec = _G_COMPRESSED if G is None else G + + a_int = int.from_bytes(a_bytes, "big") + if a_int == 0 or a_int >= SECP256K1_ORDER: + raise DLEQError("a is out of range [1, n-1]") + + try: + A_internal = _scalar_mult_G(a_bytes, G_sec) # A = a*G + except (ValueError, OverflowError): + raise DLEQError("Invalid private key or G point") + A_compressed = ec_pubkey_serialize(A_internal, EC_COMPRESSED) + + # C = a*B; ec_pubkey_parse rejects the point at infinity (spec is_infinite(B)). + try: + C_internal = ec_pubkey_parse(B_sec) + ec_pubkey_tweak_mul(C_internal, a_bytes) + except (ValueError, OverflowError): + raise DLEQError("Invalid B_sec") + C_compressed = ec_pubkey_serialize(C_internal, EC_COMPRESSED) + + if r is None: + raise DLEQError( + "r must be provided: pass 32 bytes of fresh auxiliary randomness" + ) + if len(r) != 32: + raise DLEQError("r must be 32 bytes") + + aux_hash = hashes.tagged_hash(DLEQ_TAG_AUX, r) + t_int = a_int ^ int.from_bytes(aux_hash, "big") + t = t_int.to_bytes(32, "big") + + m_prime = m if m is not None else b"" + + # k = H_nonce(t || cbytes(A) || cbytes(C) || m') mod n + rand = hashes.tagged_hash(DLEQ_TAG_NONCE, t + A_compressed + C_compressed + m_prime) + k = int.from_bytes(rand, "big") % SECP256K1_ORDER + if k == 0: + raise DLEQError("Derived nonce k is zero") + + k_bytes = k.to_bytes(32, "big") + + R1_internal = _scalar_mult_G(k_bytes, G_sec) # R1 = k*G + R1_compressed = ec_pubkey_serialize(R1_internal, EC_COMPRESSED) + + R2_internal = ec_pubkey_parse(B_sec) # R2 = k*B (fresh buffer) + ec_pubkey_tweak_mul(R2_internal, k_bytes) + R2_compressed = ec_pubkey_serialize(R2_internal, EC_COMPRESSED) + + # e = int(H_challenge(A || B || C || G || R1 || R2 || m')) + e_hash = hashes.tagged_hash( + DLEQ_TAG_CHALLENGE, + A_compressed + + B_sec + + C_compressed + + G_sec + + R1_compressed + + R2_compressed + + m_prime, + ) + e = int.from_bytes(e_hash, "big") + + s = (k + e * a_int) % SECP256K1_ORDER + + proof = e.to_bytes(32, "big") + s.to_bytes(32, "big") + + # self-verify before returning (as required by spec) + if not verify_dleq_proof(A_compressed, B_sec, C_compressed, proof, m, G=G): + raise DLEQError("Self-verification of generated proof failed") + + return proof + + +def verify_dleq_proof(A_sec, B_sec, C_sec, proof, m=None, G=None): + """ + Verify a 64-byte DLEQ proof (BIP-374 VerifyProof). + + Returns True if the proof is valid, False otherwise. + Never raises exceptions — all failures return False. + + Args: + A_sec: 33-byte compressed pubkey (a*G). + B_sec: 33-byte compressed pubkey (alternative base point B). + C_sec: 33-byte compressed pubkey (a*B, the ECDH result). + proof: 64-byte proof bytes. + m: Optional 32-byte message that was bound during generation. + G: 33-byte compressed base point. Defaults to secp256k1 G. + Must match the G used during proof generation. + """ + if len(proof) != 64: + return False + if m is not None and len(m) != 32: + return False + + G_sec = _G_COMPRESSED if G is None else G + + try: + e = int.from_bytes(proof[:32], "big") + s = int.from_bytes(proof[32:], "big") + + if s >= SECP256K1_ORDER: + return False + + neg_e = (-e) % SECP256K1_ORDER + if neg_e == 0: + return False + + m_prime = m if m is not None else b"" + s_bytes = s.to_bytes(32, "big") + neg_e_bytes = neg_e.to_bytes(32, "big") + + # ec_pubkey_parse rejects the point at infinity (spec is_infinite checks + # for A, B, C). ec_pubkey_combine raises ValueError when R1 or R2 would + # be the point at infinity (spec is_infinite(R1) / is_infinite(R2)). + # ec_pubkey_tweak_mul mutates in-place; each point gets a fresh buffer. + + # R1 = s*G + (-e)*A + sG_internal = _scalar_mult_G(s_bytes, G_sec) + neg_eA_internal = ec_pubkey_parse(A_sec) + ec_pubkey_tweak_mul(neg_eA_internal, neg_e_bytes) + R1_internal = ec_pubkey_combine(sG_internal, neg_eA_internal) + R1_compressed = ec_pubkey_serialize(R1_internal, EC_COMPRESSED) + + # R2 = s*B + (-e)*C + sB_internal = ec_pubkey_parse(B_sec) + ec_pubkey_tweak_mul(sB_internal, s_bytes) + neg_eC_internal = ec_pubkey_parse(C_sec) + ec_pubkey_tweak_mul(neg_eC_internal, neg_e_bytes) + R2_internal = ec_pubkey_combine(sB_internal, neg_eC_internal) + R2_compressed = ec_pubkey_serialize(R2_internal, EC_COMPRESSED) + + # Recompute e' = int(H_challenge(A || B || C || G || R1 || R2 || m')) + e_check = int.from_bytes( + hashes.tagged_hash( + DLEQ_TAG_CHALLENGE, + A_sec + B_sec + C_sec + G_sec + R1_compressed + R2_compressed + m_prime, + ), + "big", + ) + + return e == e_check + + except (ValueError, OverflowError): + return False From fb103da0b46c5b16adebeebdd380b6de44445a9c Mon Sep 17 00:00:00 2001 From: notTanveer Date: Fri, 15 May 2026 17:24:30 +0530 Subject: [PATCH 2/2] test: dleq proofs added dleq test vectors: * https://github.com/bitcoin/bips/blob/master/bip-0374/test_vectors_generate_proof.csv * https://github.com/bitcoin/bips/blob/master/bip-0374/test_vectors_verify_proof.csv --- .../data/test_vectors_generate_proof.csv | 12 +++ .../tests/data/test_vectors_verify_proof.csv | 16 ++++ tests/tests/test_dleq.py | 77 +++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 tests/tests/data/test_vectors_generate_proof.csv create mode 100644 tests/tests/data/test_vectors_verify_proof.csv create mode 100644 tests/tests/test_dleq.py diff --git a/tests/tests/data/test_vectors_generate_proof.csv b/tests/tests/data/test_vectors_generate_proof.csv new file mode 100644 index 00000000..f913508d --- /dev/null +++ b/tests/tests/data/test_vectors_generate_proof.csv @@ -0,0 +1,12 @@ +index,point_G,scalar_a,point_B,auxrand_r,message,result_proof,comment +0,02cef38f55e78b321a1f785cb1c6e33dfcef9784c18bdc4e279801c449ccdfb88e,07ff93d43f1012a5d4a44aba55240212ed39c87b3344e46757d99f24177fc576,02dad4b35c2379ba8334c9a5dda8f6e6d5cd575a7cc9d3ca4faaac51839daaa30f,cb979b0fc8ccc7f237751e719d992fcc324b6500af33999cd54a3e5c05fb1ea4,efb07d4b382d3da1079fbf24df623ba6c2e4c764993bbfa6dd7a4fe4aaf33859,7e7e934169e0bf4706e6b29e5a621c7fe199a524744a25af80071e111c0e2e94118e730d8add118dd2ee4f7d1cc183e1b87168362d1a6f85c16d8671a3fc7a8a,Success case 1 +1,02464e351831efedb755223cabbf664f10564b4742c725c023034bc928ed339e0e,f4e9172285393c6ada994c811b3e50fc47e96421ea7e54f4a4e459528d4cf562,03fe589b0fa23f060f6d4d1e76b9b19d5bb3db0e56d39a4303913de0e706463008,75f12482b9209dae12230ea1f8bf69723a1b447d361db8f510dd9ab33556fd4c,76184ce9eea5b339ebf5304b57452c1ada1466610f0a58574d6c496798cee04b,6b4521a8363a7ebc5d95ac6ec6b64db81fcf21795187d7c4600c42b73fb4fb9870ab8d106c0fd2d292c1710e10437b20575ddb3cb32eb77a5618d94ddba600f2,Success case 2 +2,0222db2054fef98344352a13bc0304a71da7b5e9a2f7fd1f3c9f3519a3d9377fb7,589476913e763b60d5c2a5bfb39230ec669caac1b44312e9bcd2d3f4473abfef,03bc7a19970c812118f74ba659b491e00dade6096ff62d1afe032a92b8671498ed,4da1c4c4b0f9db4eb6b2e5cb648d7e8a0aa35aa5c4ec4d07f096e0e03deca366,66503623468a78cfcef47888c85e0010ecd897f441d263448bfc7a89b882ab20,12aa2aa469b3c037871a09d18ab18d3840219b1ed169f6ef9deae6d927949884a459705ae89a57522224ce3482dee00a41ba511188ae60efdeb736223eb66e7b,Success case 3 +3,03dfa65bd3711eba75fa1996a0c1d95a4419bd835304152d9aa6efa590670f2af6,24d0ed3fc189eb1b64e5dc9dd4af0f3c8c143b0c79cb5fcca0dfa08a11cc60a1,03b51081323d38fb0b75f0c1ec6755fdb79c239c327ca11269fe68ba8a878b704e,31a68d6db27f6404bbceff646ff1b26a34704a0105a36c5a845d0257cea19c9b,f2996b3766d123a949e65541baf1d89d446360d05af51bd93f0445d8c472c952,7907653d29c5722ae44510e7f2839f253450aefc833b7e0a3b38384032f847f2cf41136b2fe6a558ad125287d20c0117f2a30c4ac0c4cebbcfa1dd3a69d84200,Success case 4 +4,02b15de5a3aefcfe2473916c76e619b5800ac7250ef93a9e6e0dd1505104fc58e7,73fffa796edb72d111b5e0bbda1608f098ac98120796f971b438691e1bfb7b96,03a4692be176ff89a972de9cc407083096847b950d1cae72b947665a3d5f4c2f01,1cdfb4d7cce5e50783299896a471a44e6aa2c5e2100d6c37987c6b40503c6162,0ceb45f560f2cf6b76a139ffe2c47c5ca6d26d6a3a210e59f197413bbec040b4,02277ca5a7acfe8ae13c2db4a8f74489d0ba100ed8b082381ddb6522c4510718ab88b8dbbd785c388ade79586cf6416f3c47a79670af84abccc788a5d9f2e327,Success case 5 +5,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,c08ca8e0bb59769fc6a4e078456284e00ea34f65add988c246e1bba85824ccdc,034bccb1c570ac1f3bc42d61fe35de605b99626501ccb20297e1acbbf2d7152aa1,c8d7056abd4726eb5a0f198740af14d6c1f0c16e5d7a37eaec621b661e669ac4,,503562d36910cd2d61a4d07c8ff680265c713e63dde0dcb88e6ea3c58597bdc05b86db9af95eccc475ce2177f941c118fefed20227d4ce8ce9557cb008758de6,Success case 6 +6,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,8e641ba6bf7f64eec76005a29585a5035376375f33e331215aedfe03b8e80e7a,0231c64e3efa506fdad6aad0f6084d5f6739de7f448d7e66f9d22f842638f41d60,02a7b2e2f5a5e9b1078dbb160502a32491fe80a091e91dd92cf77b0b7d90970f,35841ca532846e1cdd23a3d107824343584f88eff580929469865eae8355ee3c,5c7b27a33210750e9de8679d9f43497cf9f12ac642cde0a1fc26443aa2fc89bf71aabf7bac89f5d8a96cbe86daba155fa74d6f3e111136179e53b04eb6d7807f,Success case 7 +7,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,cfb9a7ecc49bea4f2e2ee34c38a6f48b5cd5bd06f4e4d4ffb45905b3d26db842,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,d38466b77484154a3fcb3151094c1c8a845c73a3c036b3a8ebffd8ef62c9047f,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,Success case 8 +8,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,0000000000000000000000000000000000000000000000000000000000000000,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,d38466b77484154a3fcb3151094c1c8a845c73a3c036b3a8ebffd8ef62c9047f,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,INVALID,Failure case (a=0) +9,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,d38466b77484154a3fcb3151094c1c8a845c73a3c036b3a8ebffd8ef62c9047f,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,INVALID,Failure case (a=N [group order]) +10,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,cfb9a7ecc49bea4f2e2ee34c38a6f48b5cd5bd06f4e4d4ffb45905b3d26db842,INFINITY,d38466b77484154a3fcb3151094c1c8a845c73a3c036b3a8ebffd8ef62c9047f,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,INVALID,Failure case (B is point at infinity) diff --git a/tests/tests/data/test_vectors_verify_proof.csv b/tests/tests/data/test_vectors_verify_proof.csv new file mode 100644 index 00000000..8076e813 --- /dev/null +++ b/tests/tests/data/test_vectors_verify_proof.csv @@ -0,0 +1,16 @@ +index,point_G,point_A,point_B,point_C,proof,message,result_success,comment +0,02cef38f55e78b321a1f785cb1c6e33dfcef9784c18bdc4e279801c449ccdfb88e,02b540b22c2c5ef0dc886abdaad27498453d893265560bc08a187319af6f845f58,02dad4b35c2379ba8334c9a5dda8f6e6d5cd575a7cc9d3ca4faaac51839daaa30f,03fefe00951dcd0ef10b12523393c2b8113119de4fdeeab320694e96bdccd2775b,7e7e934169e0bf4706e6b29e5a621c7fe199a524744a25af80071e111c0e2e94118e730d8add118dd2ee4f7d1cc183e1b87168362d1a6f85c16d8671a3fc7a8a,efb07d4b382d3da1079fbf24df623ba6c2e4c764993bbfa6dd7a4fe4aaf33859,TRUE,Success case 1 +1,02464e351831efedb755223cabbf664f10564b4742c725c023034bc928ed339e0e,032baaf1b10845a51b551196984a91efe2adf9d41b92bec3927218e6e4ca344002,03fe589b0fa23f060f6d4d1e76b9b19d5bb3db0e56d39a4303913de0e706463008,031f59aa1df22190e00380d8c5941adf899f596593765a1251005fd24f2bf7c884,6b4521a8363a7ebc5d95ac6ec6b64db81fcf21795187d7c4600c42b73fb4fb9870ab8d106c0fd2d292c1710e10437b20575ddb3cb32eb77a5618d94ddba600f2,76184ce9eea5b339ebf5304b57452c1ada1466610f0a58574d6c496798cee04b,TRUE,Success case 2 +2,0222db2054fef98344352a13bc0304a71da7b5e9a2f7fd1f3c9f3519a3d9377fb7,026aa26fcd626f8f55295859e9f8dd1f103149dd64d77c2bbba1bcf33bb37ebaa2,03bc7a19970c812118f74ba659b491e00dade6096ff62d1afe032a92b8671498ed,035628d1a69910daef614c7cae68d71ece55c5908af2360629e25c1b7de21eeb4b,12aa2aa469b3c037871a09d18ab18d3840219b1ed169f6ef9deae6d927949884a459705ae89a57522224ce3482dee00a41ba511188ae60efdeb736223eb66e7b,66503623468a78cfcef47888c85e0010ecd897f441d263448bfc7a89b882ab20,TRUE,Success case 3 +3,03dfa65bd3711eba75fa1996a0c1d95a4419bd835304152d9aa6efa590670f2af6,031bf61ba89009ee1266c9003a72e8e07d77877678ccda7f15325aadcd64ed186b,03b51081323d38fb0b75f0c1ec6755fdb79c239c327ca11269fe68ba8a878b704e,02d1b1f37a80217ba73785babfa63251052775f9d3ca65060054033288b7a3f66b,7907653d29c5722ae44510e7f2839f253450aefc833b7e0a3b38384032f847f2cf41136b2fe6a558ad125287d20c0117f2a30c4ac0c4cebbcfa1dd3a69d84200,f2996b3766d123a949e65541baf1d89d446360d05af51bd93f0445d8c472c952,TRUE,Success case 4 +4,02b15de5a3aefcfe2473916c76e619b5800ac7250ef93a9e6e0dd1505104fc58e7,0296c8e00dda60bb5565b77371ff913091978646b58ccf218bc591f68a75232e6e,03a4692be176ff89a972de9cc407083096847b950d1cae72b947665a3d5f4c2f01,03a7a7f9527fd387c2b2ce0c76669d646c78a3b470a4b34d3a2dabafc8505ef472,02277ca5a7acfe8ae13c2db4a8f74489d0ba100ed8b082381ddb6522c4510718ab88b8dbbd785c388ade79586cf6416f3c47a79670af84abccc788a5d9f2e327,0ceb45f560f2cf6b76a139ffe2c47c5ca6d26d6a3a210e59f197413bbec040b4,TRUE,Success case 5 +5,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,02637b2c3ea8ca80b9caecc50f4134c86ae9cf7a269133e7afc71f30e3a3cda60c,034bccb1c570ac1f3bc42d61fe35de605b99626501ccb20297e1acbbf2d7152aa1,0285b826c8dd175805901906b6c9b4140a30cbcc94c6e7dcf36476038bf90d4718,503562d36910cd2d61a4d07c8ff680265c713e63dde0dcb88e6ea3c58597bdc05b86db9af95eccc475ce2177f941c118fefed20227d4ce8ce9557cb008758de6,,TRUE,Success case 6 +6,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,02983a72b4cb44d4322641a7b2001900cd6ae0908a610546c73ed126accdba0514,0231c64e3efa506fdad6aad0f6084d5f6739de7f448d7e66f9d22f842638f41d60,03af1bc14b384eda28398df6a7900e567c5b6f6613cafce5027b98be015286f71b,5c7b27a33210750e9de8679d9f43497cf9f12ac642cde0a1fc26443aa2fc89bf71aabf7bac89f5d8a96cbe86daba155fa74d6f3e111136179e53b04eb6d7807f,35841ca532846e1cdd23a3d107824343584f88eff580929469865eae8355ee3c,TRUE,Success case 7 +7,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,TRUE,Success case 8 +8,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,FALSE,Swapped points case 1 +9,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,FALSE,Swapped points case 2 +10,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,FALSE,Swapped points case 3 +11,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,FALSE,Swapped points case 4 +12,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,FALSE,Swapped points case 5 +13,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,78a5544afa75bf152653fe55fb76926f2f65131ff090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,FALSE,Tampered proof (random bit-flip) +14,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb6d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,FALSE,Tampered message (random bit-flip) diff --git a/tests/tests/test_dleq.py b/tests/tests/test_dleq.py new file mode 100644 index 00000000..f96a7afa --- /dev/null +++ b/tests/tests/test_dleq.py @@ -0,0 +1,77 @@ +import csv +from pathlib import Path +from unittest import TestCase +from binascii import unhexlify + +from embit.silent_payments.dleq import generate_dleq_proof, verify_dleq_proof, DLEQError + +_DATA_DIR = Path(__file__).parent / "data" +_GENERATE_CSV = _DATA_DIR / "test_vectors_generate_proof.csv" +_VERIFY_CSV = _DATA_DIR / "test_vectors_verify_proof.csv" +_FAKE_INFINITY = b"\x02" + b"\x00" * 32 # x=0 is not on secp256k1 + + +class TestBIP374GenerateProofVectors(TestCase): + """Official BIP-374 generate-proof test vectors.""" + + def test_vectors(self): + with open(_GENERATE_CSV, newline="") as fh: + for row in csv.DictReader(fh): + idx = row["index"] + G_hex = row["point_G"] + a_hex = row["scalar_a"] + B_hex = row["point_B"] + r_hex = row["auxrand_r"] + msg_hex = row["message"] + result = row["result_proof"] + comment = row["comment"] + + with self.subTest(index=idx, comment=comment): + G_bytes = unhexlify(G_hex) + + a_bytes = unhexlify(a_hex) + r_bytes = unhexlify(r_hex) + m = unhexlify(msg_hex) if msg_hex else None + B_sec = _FAKE_INFINITY if B_hex == "INFINITY" else unhexlify(B_hex) + + if result == "INVALID": + with self.assertRaises(DLEQError): + generate_dleq_proof( + a_bytes, B_sec, r=r_bytes, m=m, G=G_bytes + ) + else: + proof = generate_dleq_proof( + a_bytes, B_sec, r=r_bytes, m=m, G=G_bytes + ) + self.assertEqual(proof, unhexlify(result)) + + +class TestBIP374VerifyProofVectors(TestCase): + """Official BIP-374 verify-proof test vectors.""" + + def test_vectors(self): + with open(_VERIFY_CSV, newline="") as fh: + for row in csv.DictReader(fh): + idx = row["index"] + G_hex = row["point_G"] + A_hex = row["point_A"] + B_hex = row["point_B"] + C_hex = row["point_C"] + proof_hex = row["proof"] + msg_hex = row["message"] + expected = row["result_success"] == "TRUE" + comment = row["comment"] + + with self.subTest(index=idx, comment=comment): + G_bytes = unhexlify(G_hex) + + A_sec = unhexlify(A_hex) + B_sec = unhexlify(B_hex) + C_sec = unhexlify(C_hex) + proof = unhexlify(proof_hex) + m = unhexlify(msg_hex) if msg_hex else None + + result = verify_dleq_proof( + A_sec, B_sec, C_sec, proof, m=m, G=G_bytes + ) + self.assertEqual(result, expected)