diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index dfd21219..6046269f 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -86,6 +86,7 @@ add_subdirectory(circuits/mdoc) add_subdirectory(circuits/sha) # experiments and tests, not used in production, may be buggy +add_subdirectory(circuits/tests/contrib/bip340) add_subdirectory(circuits/tests/anoncred) add_subdirectory(circuits/tests/sha3) add_subdirectory(circuits/tests/base64) diff --git a/lib/circuits/tests/contrib/bip340/CMakeLists.txt b/lib/circuits/tests/contrib/bip340/CMakeLists.txt new file mode 100644 index 00000000..863bcd16 --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/CMakeLists.txt @@ -0,0 +1,16 @@ +# Copyright 2026 Google LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +proofs_add_tests(bip340_test) +target_link_libraries(bip340_test crypto zstd) diff --git a/lib/circuits/tests/contrib/bip340/README.md b/lib/circuits/tests/contrib/bip340/README.md new file mode 100644 index 00000000..f41377ec --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/README.md @@ -0,0 +1,243 @@ +# BIP-340 Schnorr verification circuit + +This directory contains a contributed Longfellow circuit for the algebraic +part of BIP-340 Schnorr verification over secp256k1. It is a reusable building +block for zero-knowledge applications that need to prove a valid Schnorr +relation without revealing the response scalar `s` or the lifted points. + +At a glance: + +| Property | Selection | +| --- | --- | +| Signature relation | `s*G - e*P = R` | +| Circuit field | secp256k1 base field `Fp256k1Base` | +| EC arithmetic | Complete projective formulas, fixed 256-step double-and-add | +| Proof system | Longfellow Sumcheck + Ligero | +| Witness commitment | Salted SHA-256 Merkle tree over RS-extended columns | +| Reed-Solomon backend | Multi-prime CRT convolution | +| Ligero test parameters | `rateinv = 4`, `nreq = 128` | +| Trusted setup | None | + +The proof stack is: + +```text +BIP-340 constraints -> QuadCircuit -> Sumcheck -> Ligero + -> Reed-Solomon encoding through CRT convolution + -> salted SHA-256 Merkle commitment and compressed openings +``` + +## Statement and witness + +The circuit proves + +```text +s*G - e*P = R +``` + +where `P` and `R` are represented from x-only inputs. The application supplies +three public field elements: + +- `rx`: the x-coordinate of the signature nonce commitment `R`; +- `px`: the x-only public key; +- `e`: the BIP-340 challenge scalar. + +The compiler also reserves public input zero for the constant one, so the +compiled circuit reports four public inputs in total. + +The 2,301 private inputs contain: + +- the 256 bits of `s` and 255 witnessed projective accumulator points for + `s*G`; +- the 256 bits of `e` and 255 witnessed projective accumulator points for + `e*P`; +- the lifted affine coordinates `py` and `ry`; +- `rz_inv`, which proves that the computed `R` is not the point at infinity; +- the 256 bits of `ry`, used to reconstruct it and enforce even parity. + +The circuit constrains the scalar-multiplication traces, bitness, `s < n`, +curve membership for `P` and `R`, projective equality with the public `rx`, +finiteness of `R`, and the BIP-340 even-y convention for `R`. + +## Design decisions + +### Native secp256k1 field + +The circuit is compiled over the secp256k1 base field. Curve coordinates are +therefore native field elements, avoiding non-native coordinate arithmetic. +The group order `n` is different from the base-field modulus `p`, so the +response is represented by 256 bits and explicitly constrained to `s < n`. +This range check is necessary even when the group equation holds, because +`s + n` represents the same group scalar but is not a canonical BIP-340 +encoding. + +### Projective, fixed-shape scalar multiplication + +Both `s*G` and `e*P` use a fixed 256-iteration, MSB-first double-and-add trace. +Every bit is constrained, and each intermediate accumulator is supplied as a +witness and checked before it becomes the input to the next step. + +Projective coordinates and complete group-law formulas were selected to avoid +a field inversion or exceptional-case branch at every addition. The circuit +performs one fixed sequence independent of the scalar bits: a bit-controlled +multiplexer selects either the input point or the point at infinity. This gives +the compiler a regular constraint shape and keeps the circuit depth small at +the cost of a larger witness. + +### X-only points and the nonce commitment `R` + +In BIP-340, `R` is the Schnorr nonce commitment carried by its x-coordinate. +The circuit computes `R = s*G - e*P`, proves it is finite and on-curve, and +matches its projective x-coordinate to public `rx`. A witnessed affine `ry` is +reconstructed from 256 constrained bits and required to be even, selecting the +canonical BIP-340 lift. + +Similarly, `py` is private and constrained by `py^2 = px^3 + 7`. The supplied +witness generator chooses the even lift of `px`, but the current circuit does +not decompose `py` or constrain its parity. Consequently, the standalone +circuit relation permits either lift of `P`; a protocol that requires strict +BIP-340 x-only public-key semantics must additionally bind `py` to the even +lift. The verifier must also reject a non-canonical external x encoding before +mapping it into a field element. + +### Tagged SHA-256 remains outside the circuit + +The circuit proves the algebraic relation for a public `e`; it does not prove +SHA-256. The application must parse the signature and public key canonically, +reject `rx >= p`, `s >= n`, and `px >= p`, lift `px`, and compute + +```text +e = SHA256_tagged("BIP0340/challenge", rx || px || message) mod n +``` + +before verification. Keeping SHA-256 outside avoids adding a large bit-oriented +hash circuit to an otherwise field-native EC circuit. The trade-off is an +important API boundary: the verifier is responsible for binding public `e` to +the message, public key, and signature bytes. `bip340_witness.h` implements +this parsing and hashing for the supplied witness path. + +### Proof-system commitment + +The Schnorr nonce commitment `R` and the proof-system witness commitment are +different objects. BIP-340 does not introduce a special Pedersen or KZG +commitment here. The ZK proof inherits Longfellow's transparent Ligero +commitment: + +1. The witness and constraint data are arranged in a blinded Ligero tableau. +2. Each row is Reed-Solomon extended. +3. Each extension column becomes a Merkle leaf + `SHA256(fresh_32_byte_nonce || encoded_column)`. +4. Inner nodes are `SHA256(left || right)`. +5. The Merkle root enters the Fiat-Shamir transcript before challenges are + sampled. +6. The prover opens 128 distinct challenged columns and sends a compressed + multi-opening containing only sibling nodes that cannot be reconstructed. + +The random Ligero rows provide the zero-knowledge blinding; the per-leaf +nonces are part of the Merkle commitment construction. The query count and +Reed-Solomon rate are proof-system parameters controlling the soundness, +proof-size, and work trade-off. They are not BIP-340-specific primitives. + +### CRT Reed-Solomon encoding + +Native secp256k1 proving uses: + +```cpp +ReedSolomonFactory< + Fp256k1Base, + CrtConvolutionFactory, Fp256k1Base>> +``` + +The P-256 `Fp2` extension strategy is not suitable here because the +secp256k1 base field has insufficient 2-adicity for the required practical +power-of-two FFT domains. The CRT backend performs the convolution over +auxiliary FFT-friendly primes and reconstructs the result in the native +field. + +The auxiliary primes support FFT order `2^22`. `check_crt_block_enc()` rejects +a configuration when the next power-of-two padding for `block_enc` exceeds +that limit, producing a clear parameter error before the FFT implementation is +entered. + +## Size and performance + +Compiler-reported metrics for one verification circuit are: + +| Metric | Value | +| --- | ---: | +| Wires | 26,802 | +| Quadratic terms | 41,443 | +| Circuit depth | 9 | +| Application public field inputs | 3 | +| Compiled public inputs, including constant one | 4 | +| Private witness elements | 2,301 | +| Total compiled inputs | 2,305 | +| Approximate `block_enc` | 43,745 | +| CRT convolution padding | 65,536 | + +`Bip340ParamTest.ReportCircuitParams` derives these values from the compiled +circuit. The 65,536-point transform is 64 times smaller than the CRT backend's +`2^22` maximum transform order, leaving capacity for larger composed circuits; +composition still needs to check the guard because padding grows in powers of +two. + +An illustrative Release-mode smoke test on a 12th Gen Intel Core i9-12900HK, +using GCC 14.2 and one process, produced the following ranges over five runs: + +| Measurement | Observed range | +| --- | ---: | +| Ligero commit + prove | 42.8-43.7 ms | +| Verify | 26.1-27.3 ms | +| End-to-end test process | 0.10 s | +| Peak resident memory | 20.1-20.5 MiB | + +These figures are a development snapshot, not a portable benchmark. They +exclude application I/O and do not report serialized proof size. Compiler, +CPU, allocator, proof parameters, and circuit composition can materially +change the result. The dominant prover work is the Merkle/RS commitment and +sumcheck; verifier work is driven mainly by the challenged Ligero columns and +their Merkle openings rather than by replaying all 41,443 quadratic terms. + +## Files + +| File | Purpose | +| --- | --- | +| `bip340_verify.h` | Circuit constraints for the algebraic verification relation | +| `bip340_witness.h` | Canonical parsing, tagged hashing, point lifting, and witness generation | +| `bip340_guard.h` | CRT transform-capacity validation | +| `bip340_test.cc` | Evaluation, vectors, soundness, mutation, ZK, parameter, and scale tests | +| `specs/code/bip340.py` | Independent affine Sage reference | + +## Sage reference + +`specs/code/bip340.py` validates all 19 Bitcoin Core vectors and computes +semantic golden facts that are compared with the C++ implementation. It is an +independent affine reference, not a clone of the optimized projective circuit. + +The runner adds `docs/specs/sage` to `PYTHONPATH`, so shared Sage modules can +be imported rather than duplicated. BIP-340 type checks use that directory's +mypy configuration and stubs as well. + +## Tests + +```bash +cmake -S lib -B build -G Ninja -DCMAKE_BUILD_TYPE=Release +cmake --build build --target bip340_test -j$(nproc) +ctest --test-dir build -R 'Bip340' --output-on-failure +./lib/circuits/tests/contrib/bip340/specs/code/run_bip340_sage_tests.sh +``` + +The C++ suite includes valid and invalid upstream vectors, real Ligero +prover/verifier runs, canonical-scalar and even-y soundness tests, private and +public mutation tests, proof tampering, CRT boundary checks, and a two-instance +composition smoke test. + +## Acknowledgements + +This circuit is designed, written, and maintained by Denis Roio +. The most up-to-date implementation is available in the +[Dyne Longfellow ZK repository](https://github.com/dyne/longfellow-zk). + +The circuit is used by [Zenroom](https://zenroom.org) through the zkcc circuit +compiler DSL as an optimized implementation of *Improved Concurrent-Secure +Blind Schnorr Signatures* by Pierpaolo Della Monica and Ivan Visconti +([ePrint 2025/1992](https://eprint.iacr.org/2025/1992)). diff --git a/lib/circuits/tests/contrib/bip340/bip340_guard.h b/lib/circuits/tests/contrib/bip340/bip340_guard.h new file mode 100644 index 00000000..07e0595c --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/bip340_guard.h @@ -0,0 +1,58 @@ +// Copyright 2026 Google LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef PRIVACY_PROOFS_ZK_LIB_CIRCUITS_TESTS_CONTRIB_BIP340_BIP340_GUARD_H_ +#define PRIVACY_PROOFS_ZK_LIB_CIRCUITS_TESTS_CONTRIB_BIP340_BIP340_GUARD_H_ + +#include +#include +#include + +#include "algebra/crt.h" + +namespace proofs { + +// Returns the smallest power of two that is at least n. +inline size_t next_pow2(size_t n) { + size_t result = 1; + while (result < n) { + result *= 2; + } + return result; +} + +/// Guard: checks that the block_enc parameter for a CRT-backed secp256k1 +/// proof is within the supported FFT size. The CRT auxiliary primes support +/// a maximum FFT order of 2^22 (4,194,304). If the padding required by +/// block_enc exceeds this, the proof will fail inside twiddle-factor +/// computation. This function catches the problem early. +/// +/// Returns a human-readable error string, or empty string if OK. +template +inline std::string check_crt_block_enc(size_t block_enc) { + constexpr uint64_t kMaxOrder = crt::kOmegaOrder; // 2^22 + size_t pad = next_pow2(block_enc); + if (pad > kMaxOrder) { + return "CRT block_enc=" + std::to_string(block_enc) + + " requires padding=" + std::to_string(pad) + + " which exceeds CRT omega_order=" + std::to_string(kMaxOrder) + + " (2^22). Reduce circuit size or use a larger auxiliary prime " + "basis."; + } + return ""; +} + +} // namespace proofs + +#endif // PRIVACY_PROOFS_ZK_LIB_CIRCUITS_TESTS_CONTRIB_BIP340_BIP340_GUARD_H_ diff --git a/lib/circuits/tests/contrib/bip340/bip340_test.cc b/lib/circuits/tests/contrib/bip340/bip340_test.cc new file mode 100644 index 00000000..3e2848c6 --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/bip340_test.cc @@ -0,0 +1,1357 @@ +// Copyright 2026 Google LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// ... + +#include "circuits/tests/contrib/bip340/bip340_verify.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "algebra/crt.h" +#include "algebra/crt_convolution.h" +#include "algebra/reed_solomon.h" +#include "arrays/dense.h" +#include "circuits/compiler/circuit_dump.h" +#include "circuits/compiler/compiler.h" +#include "circuits/logic/compiler_backend.h" +#include "circuits/logic/evaluation_backend.h" +#include "circuits/logic/logic.h" +#include "circuits/tests/contrib/bip340/bip340_guard.h" +#include "circuits/tests/contrib/bip340/bip340_witness.h" +#include "ec/p256k1.h" +#include "random/secure_random_engine.h" +#include "random/transcript.h" +#include "util/log.h" +#include "util/readbuffer.h" +#include "zk/zk_proof.h" +#include "zk/zk_prover.h" +#include "zk/zk_verifier.h" +#include "gtest/gtest.h" + +namespace proofs { +namespace { + +constexpr size_t kRate = 4; +constexpr size_t kQueries = 128; + +using Field = Fp256k1Base; +using Nat = typename Field::N; +using Elt = typename Field::Elt; +using EC = P256k1; + +// ======================== Evaluation Tests ============================== + +/// Stable public-input filling helper: pushes [F.one(), rx, px, e] +/// in the order expected by the production circuit. +template +void PushBip340PublicInputs(DenseFiller& filler, const Field& F, + typename Field::Elt rx, + typename Field::Elt px, + typename Field::Elt e) { + filler.push_back(F.one()); + filler.push_back(rx); + filler.push_back(px); + filler.push_back(e); +} + +/// Centralized eval-witness builder: fills a VerifyC::Witness from a +/// Bip340Witness using a LogicType to create konst wires. This is the +/// single place where eval witness fields are copied; all eval tests +/// use this helper (except tests that intentionally mutate fields). +template +typename VerifyC::Witness MakeEvalWitness(const LogicType& l, + const Bip340Witness& wit) { + typename VerifyC::Witness w; + for (size_t i = 0; i < Bip340Witness::kBits; ++i) { + w.bits_s[i] = l.konst(wit.bits_s_[i]); + w.bits_e[i] = l.konst(wit.bits_e_[i]); + w.bits_ry[i] = l.konst(wit.bits_ry_[i]); + if (i < Bip340Witness::kBits - 1) { + w.int_sx[i] = l.konst(wit.int_sx_[i]); + w.int_sy[i] = l.konst(wit.int_sy_[i]); + w.int_sz[i] = l.konst(wit.int_sz_[i]); + w.int_ex[i] = l.konst(wit.int_ex_[i]); + w.int_ey[i] = l.konst(wit.int_ey_[i]); + w.int_ez[i] = l.konst(wit.int_ez_[i]); + } + } + w.py = l.konst(wit.py_); + w.ry = l.konst(wit.ry_); + w.rz_inv = l.konst(wit.rz_inv_); + return w; +} + +class Bip340EvalTest : public ::testing::Test { + protected: + using EvalBackend = EvaluationBackend; + using LogicType = Logic; + using EltW = typename LogicType::EltW; + using VerifyC = Bip340Verify; + + const Field& F = p256k1_base; + const EC& ec = p256k1; + + /// Build a witness from known scalars and verify the circuit. + /// expected = true: circuit should accept (no assertion failed). + void CheckVerify(const Nat& s_nat, const Nat& e_nat, + const Elt& px, const Elt& py, + const Elt& rx, bool expected) { + Bip340Witness wit(ec); + ASSERT_TRUE(wit.compute_from_scalars(s_nat, e_nat, px, py)); + + const EvalBackend ebk(F, expected); + const LogicType l(&ebk, F); + VerifyC circuit(l, ec); + + EltW rxx = l.konst(rx); + EltW pxx = l.konst(px); + EltW ee = l.konst(wit.e_); + + typename VerifyC::Witness w = MakeEvalWitness(l, wit); + + circuit.assert_verify(rxx, pxx, ee, w); + + if (expected) { + ASSERT_FALSE(ebk.assertion_failed()); + } else { + ASSERT_TRUE(ebk.assertion_failed()); + } + } + + /// Compute R = sG - eP and return R.x (normalized). Also returns py. + Elt compute_rx(const Nat& s_nat, const Nat& e_nat, + const Elt& px, Elt& py_out) { + // Pick the even square root of px. + Elt x2 = F.mulf(px, px); + Elt x3 = F.mulf(x2, px); + Elt y2 = F.addf(x3, ec.b_); + py_out = sqrt_even(y2); + + auto G = ec.generator(); + auto sG = ec.scalar_multf(G, s_nat); + + typename EC::ECPoint P = {px, py_out, F.one()}; + auto eP = ec.scalar_multf(P, e_nat); + + auto neg_eP = typename EC::ECPoint{eP.x, F.negf(eP.y), eP.z}; + ec.addE(sG, neg_eP); + ec.normalize(sG); + return sG.x; + } + + /// sqrt mod p (p = 3 mod 4), choosing even root. + Elt sqrt_even(const Elt& a) { + Nat exp("0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffff0c"); + Elt root = F.one(); + Elt base = a; + for (int i = 255; i >= 0; --i) { + root = F.mulf(root, root); + if (exp.bit(i)) { + root = F.mulf(root, base); + } + } + Nat r0 = F.from_montgomery(root); + if (r0.bit(0) == 0) return root; + return F.negf(root); + } +}; + +TEST_F(Bip340EvalTest, ValidWitness) { + // Pick scalars that produce even R.y (verified via Sage). + Nat s_nat(2ull); + Nat e_nat(1ull); + Nat sk_nat(1ull); + + // Compute P = sk * G. + auto G = ec.generator(); + auto P = ec.scalar_multf(G, sk_nat); + ec.normalize(P); + + // Pick the even square root of P.x. + Elt py; + Elt rx = compute_rx(s_nat, e_nat, P.x, py); + + CheckVerify(s_nat, e_nat, P.x, py, rx, true); +} + +TEST_F(Bip340EvalTest, WrongPublicKeyFails) { + Nat s_nat(2ull); + Nat e_nat(1ull); + + // Use wrong public key (different sk). + auto G = ec.generator(); + auto P = ec.scalar_multf(G, Nat(3ull)); + ec.normalize(P); + + auto P_wrong = ec.scalar_multf(G, Nat(5ull)); + ec.normalize(P_wrong); + + Elt py_wrong; + Elt rx_wrong = compute_rx(s_nat, e_nat, P_wrong.x, py_wrong); + + // Try to verify with P.x but R.x for P_wrong. + CheckVerify(s_nat, e_nat, P.x, py_wrong, rx_wrong, false); +} + +TEST_F(Bip340EvalTest, WrongRXFails) { + Nat s_nat(2ull); + Nat e_nat(1ull); + + auto G = ec.generator(); + auto P = ec.scalar_multf(G, Nat(3ull)); + ec.normalize(P); + + Elt py; + Elt rx = compute_rx(s_nat, e_nat, P.x, py); + + // Use wrong rx (negate it). + Elt wrong_rx = F.negf(rx); + CheckVerify(s_nat, e_nat, P.x, py, wrong_rx, false); +} + +TEST_F(Bip340EvalTest, WrongChallengeFails) { + Nat s_nat(2ull); + Nat e_nat(1ull); + Nat e_wrong_nat(2ull); // off by 1 + + auto G = ec.generator(); + auto P = ec.scalar_multf(G, Nat(3ull)); + ec.normalize(P); + + Elt py, rx; + rx = compute_rx(s_nat, e_nat, P.x, py); + + // Build witness with wrong e. + Bip340Witness wit(ec); + ASSERT_TRUE(wit.compute_from_scalars(s_nat, e_nat, P.x, py)); + + // But pass e_wrong as the public input. + const EvalBackend ebk(F, false); + const LogicType l(&ebk, F); + VerifyC circuit(l, ec); + + EltW rxx = l.konst(rx); + EltW pxx = l.konst(P.x); + EltW ee = l.konst(F.to_montgomery(e_wrong_nat)); // wrong! + + typename VerifyC::Witness w = MakeEvalWitness(l, wit); + + circuit.assert_verify(rxx, pxx, ee, w); + ASSERT_TRUE(ebk.assertion_failed()); +} + +// ====================== BIP-340 Test Vector Tests ======================= + +// Returns the integer value of a hex character (0-15), or -1 if invalid. +inline int HexValue(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return -1; +} + +// Parses a hex string to a byte vector. Returns std::nullopt on any +// malformed input (odd length or invalid hex characters). +inline std::optional> ParseHexVec(const char* hex) { + size_t len = std::strlen(hex); + if (len % 2 != 0) return std::nullopt; + std::vector out(len / 2); + for (size_t i = 0; i < len; i += 2) { + int hi = HexValue(hex[i]); + int lo = HexValue(hex[i + 1]); + if (hi == -1 || lo == -1) return std::nullopt; + out[i / 2] = static_cast((hi << 4) | lo); + } + return out; +} + +// Helper: parse hex string to byte vector, failing the current test +// on malformed input. +inline std::vector hex_vec(const char* hex) { + auto result = ParseHexVec(hex); + EXPECT_TRUE(result.has_value()) << "hex_vec: malformed hex string \"" << hex << "\""; + if (!result.has_value()) return {}; + return *result; +} + +TEST(Bip340FixtureTest, HexVecRejectsMalformedInput) { + // Valid hex characters. + EXPECT_EQ(HexValue('0'), 0); + EXPECT_EQ(HexValue('9'), 9); + EXPECT_EQ(HexValue('A'), 10); + EXPECT_EQ(HexValue('F'), 15); + EXPECT_EQ(HexValue('a'), 10); + EXPECT_EQ(HexValue('f'), 15); + // Invalid characters. + EXPECT_EQ(HexValue('g'), -1); + EXPECT_EQ(HexValue(' '), -1); + EXPECT_EQ(HexValue('\x00'), -1); + EXPECT_EQ(HexValue('@'), -1); + + // Valid hex string. + auto v = ParseHexVec("0A1b"); + ASSERT_TRUE(v.has_value()); + EXPECT_EQ(v->size(), 2u); + EXPECT_EQ((*v)[0], 0x0A); + EXPECT_EQ((*v)[1], 0x1B); + + // Empty string. + v = ParseHexVec(""); + ASSERT_TRUE(v.has_value()); + EXPECT_EQ(v->size(), 0u); + + // Odd length. + EXPECT_FALSE(ParseHexVec("0A1").has_value()); + EXPECT_FALSE(ParseHexVec("A").has_value()); + + // Invalid characters. + EXPECT_FALSE(ParseHexVec("0g").has_value()); + EXPECT_FALSE(ParseHexVec("GG").has_value()); +} + +struct Bip340RealVector { + const char* pk_hex; + const char* msg_hex; + const char* sig_hex; + bool valid; +}; + +struct Bip340GoldenFact { + size_t index; + bool valid; + bool compute_success; + const char* rx_hex; + const char* px_hex; + const char* e_hex; + const char* py_hex; + const char* ry_hex; +}; + +/// Expected rejection layer for invalid vectors. +enum class RejectBy { + kAccept = 0, // valid signature; circuit must accept + kInputValidation, // rejected by compute() before reaching circuit + kCircuit, // accepted by compute() but rejected by circuit +}; + +/// For each invalid vector, which layer is expected to catch it. +/// Indexed by vector number; entries for valid vectors are kAccept. +constexpr RejectBy kRejectLayer[] = { + RejectBy::kAccept, // 0: valid + RejectBy::kAccept, // 1: valid + RejectBy::kAccept, // 2: valid + RejectBy::kAccept, // 3: valid + RejectBy::kAccept, // 4: valid + RejectBy::kInputValidation, // 5: pk not on curve (lift fails) + RejectBy::kCircuit, // 6: odd R.y - detected by LSB-zero gate + RejectBy::kCircuit, // 7: negated message + RejectBy::kCircuit, // 8: negated s value + RejectBy::kCircuit, // 9: R = infinity (rx=0) - R.z*rz_inv=1 fails + RejectBy::kCircuit, // 10: R = infinity (rx=1) + RejectBy::kCircuit, // 11: r not a quadratic residue - R.x==rx fails + RejectBy::kInputValidation, // 12: r >= p + RejectBy::kInputValidation, // 13: s >= n + RejectBy::kInputValidation, // 14: pk >= p + RejectBy::kAccept, // 15: valid (empty msg) + RejectBy::kAccept, // 16: valid (1-byte msg) + RejectBy::kAccept, // 17: valid (17-byte msg) + RejectBy::kAccept, // 18: valid (100-byte msg) +}; +static_assert(sizeof(kRejectLayer) / sizeof(kRejectLayer[0]) == 19, + "kRejectLayer must cover all 19 vectors"); + +// Upstream BIP-340 test vectors from Bitcoin Core. +// Auto-generated from testdata/bip340_test_vectors.csv. +// To regenerate: +// python3 lib/circuits/tests/contrib/bip340/specs/code/generate_bip340_vectors_inc.py +const Bip340RealVector kRealVectors[] = { +#include "testdata/bip340_vectors.inc" +}; +static_assert(sizeof(kRealVectors) / sizeof(kRealVectors[0]) == 19, + "kRealVectors must cover all 19 vectors"); + +const Bip340GoldenFact kGoldenFacts[] = { +#include "testdata/bip340_golden.inc" +}; +static_assert(sizeof(kGoldenFacts) / sizeof(kGoldenFacts[0]) == 19, + "kGoldenFacts must cover all 19 vectors"); + +Elt EltFromHex(const Field& F, const char* hex) { + auto be = hex_vec(hex); + uint8_t le[32] = {0}; + for (size_t i = 0; i < be.size(); ++i) { + le[i] = be[be.size() - 1 - i]; + } + return F.to_montgomery(Nat::of_bytes(le, 256)); +} + +TEST(Bip340RealVectorTest, EvalTestVectors) { + using EvalBackend = EvaluationBackend; + using LogicType = Logic; + using EltW = typename LogicType::EltW; + using VerifyC = Bip340Verify; + + const Field& F = p256k1_base; + const EC& ec = p256k1; + + for (size_t vi = 0; vi < sizeof(kRealVectors) / sizeof(kRealVectors[0]); + ++vi) { + const auto& tv = kRealVectors[vi]; + auto pk = hex_vec(tv.pk_hex); + auto msg = hex_vec(tv.msg_hex); + auto sig = hex_vec(tv.sig_hex); + RejectBy expected = kRejectLayer[vi]; + + SCOPED_TRACE("vector " + std::to_string(vi)); + + Bip340Witness wit(ec); + bool computed = wit.compute(sig.data(), pk.data(), + msg.data(), msg.size()); + + if (expected == RejectBy::kAccept) { + // Valid vector: compute() must succeed, circuit must accept. + ASSERT_TRUE(computed) << "compute() failed for valid vector " << vi; + const EvalBackend ebk(F, false); + const LogicType l(&ebk, F); + VerifyC circuit(l, ec); + + EltW rx = l.konst(F.to_montgomery( + Bip340Witness::nat_from_be_bytes(sig.data()))); + EltW px = l.konst(F.to_montgomery( + Bip340Witness::nat_from_be_bytes(pk.data()))); + EltW e = l.konst(wit.e_); + + auto w = MakeEvalWitness(l, wit); + circuit.assert_verify(rx, px, e, w); + ASSERT_FALSE(ebk.assertion_failed()) + << "Valid vector " << vi << " should pass"; + continue; + } + + if (expected == RejectBy::kInputValidation) { + // compute() must reject this vector before reaching the circuit. + ASSERT_FALSE(computed) + << "Vector " << vi << " should fail input validation"; + continue; + } + + // kCircuit: compute() may succeed but the circuit must reject. + if (!computed) { + // compute() caught it early - still valid, but log a note. + log(INFO, "Vector %zu: input validation caught what circuit should", vi); + continue; + } + + log(INFO, "Testing invalid vector %zu through circuit", vi); + const EvalBackend ebk(F, false); + const LogicType l(&ebk, F); + VerifyC circuit(l, ec); + + EltW rx = l.konst(F.to_montgomery( + Bip340Witness::nat_from_be_bytes(sig.data()))); + EltW px = l.konst(F.to_montgomery( + Bip340Witness::nat_from_be_bytes(pk.data()))); + EltW e = l.konst(wit.e_); + + auto w = MakeEvalWitness(l, wit); + circuit.assert_verify(rx, px, e, w); + ASSERT_TRUE(ebk.assertion_failed()) + << "Invalid vector " << vi << " should fail circuit check"; + } +} + +// Compares only semantic facts invariant across the Sage affine model +// and the C++ projective double-and-add witness. Implementation- +// specific projective values (e.g. rz_inv) are intentionally excluded +// because Sage and the production C++ verifier compute them differently. +TEST(Bip340RealVectorTest, CppWitnessMatchesSemanticGoldenFacts) { + const Field& F = p256k1_base; + const EC& ec = p256k1; + + for (size_t vi = 0; vi < sizeof(kRealVectors) / sizeof(kRealVectors[0]); + ++vi) { + const auto& tv = kRealVectors[vi]; + const auto& fact = kGoldenFacts[vi]; + auto pk = hex_vec(tv.pk_hex); + auto msg = hex_vec(tv.msg_hex); + auto sig = hex_vec(tv.sig_hex); + + SCOPED_TRACE("vector " + std::to_string(vi)); + ASSERT_EQ(fact.index, vi); + ASSERT_EQ(fact.valid, tv.valid); + + Bip340Witness wit(ec); + bool computed = wit.compute(sig.data(), pk.data(), + msg.data(), msg.size()); + if (!computed) { + ASSERT_FALSE(tv.valid); + continue; + } + + if (!fact.compute_success) { + ASSERT_FALSE(tv.valid); + continue; + } + + EXPECT_EQ(wit.e_, EltFromHex(F, fact.e_hex)); + EXPECT_EQ(wit.py_, EltFromHex(F, fact.py_hex)); + EXPECT_EQ(wit.ry_, EltFromHex(F, fact.ry_hex)); + } +} + +TEST(Bip340RealVectorTest, ZkProverVerifier_Vector0) { + set_log_level(INFO); + const Field& F = p256k1_base; + const EC& ec = p256k1; + + const auto& tv = kRealVectors[0]; + auto pk = hex_vec(tv.pk_hex); + auto msg = hex_vec(tv.msg_hex); + auto sig = hex_vec(tv.sig_hex); + + Bip340Witness wit(ec); + ASSERT_TRUE(wit.compute(sig.data(), pk.data(), msg.data(), msg.size())); + + // Build circuit. + using CompilerBackendType = CompilerBackend; + using LogicCircuit = Logic; + using EltWC = typename LogicCircuit::EltW; + using VerifyCC = Bip340Verify; + + QuadCircuit Q(F); + const CompilerBackendType cbk(&Q); + const LogicCircuit lc(&cbk, F); + VerifyCC circuit(lc, ec); + + EltWC rx = lc.eltw_input(); + EltWC px = lc.eltw_input(); + EltWC e = lc.eltw_input(); + Q.private_input(); + typename VerifyCC::Witness w; + w.input(lc); + circuit.assert_verify(rx, px, e, w); + auto CIRCUIT = Q.mkcircuit(1); + + // CRT guard: ensure block_enc fits within 2^22 FFT order. + { + using Crt = CRT256; + size_t block_enc = CIRCUIT->ninputs - CIRCUIT->npub_in + + Q.nquad_terms_ + 1; + auto err = check_crt_block_enc(block_enc); + ASSERT_TRUE(err.empty()) << "CRT capacity: " << err; + } + + auto W = std::make_unique>(1, CIRCUIT->ninputs); + { + DenseFiller filler(*W); + PushBip340PublicInputs(filler, F, + F.to_montgomery(Bip340Witness::nat_from_be_bytes(sig.data())), + F.to_montgomery(Bip340Witness::nat_from_be_bytes(pk.data())), + wit.e_); + wit.fill_witness(filler); + } + + using Crt = CRT256; + using ConvolutionFactory = CrtConvolutionFactory; + using RSFactory = ReedSolomonFactory; + + ConvolutionFactory factory(F); + RSFactory rsf(factory, F); + + Transcript tp((uint8_t*)"bip340 real vec0", 16); + SecureRandomEngine rng; + + ZkProof zkpr(*CIRCUIT, kRate, kQueries); + ZkProver prover(*CIRCUIT, F, rsf); + prover.commit(zkpr, *W, tp, rng); + prover.prove(zkpr, *W, tp); + log(INFO, "BIP-340 real-vector prover done"); + + Transcript trv((uint8_t*)"bip340 real vec0", 16); + auto pub = Dense(1, CIRCUIT->npub_in); + { + DenseFiller filler(pub); + PushBip340PublicInputs(filler, F, + F.to_montgomery(Bip340Witness::nat_from_be_bytes(sig.data())), + F.to_montgomery(Bip340Witness::nat_from_be_bytes(pk.data())), + wit.e_); + } + + ZkVerifier verifier(*CIRCUIT, rsf, kRate, kQueries, F); + verifier.recv_commitment(zkpr, trv); + EXPECT_TRUE(verifier.verify(zkpr, pub, trv)); + log(INFO, "BIP-340 real-vector verifier done"); +} + +// ========================= Circuit Size ================================== + +TEST(Bip340SizeTest, CircuitSize) { + using CompilerBackendType = CompilerBackend; + using LogicCircuit = Logic; + using EltW = typename LogicCircuit::EltW; + using VerifyC = Bip340Verify; + + QuadCircuit Q(p256k1_base); + const CompilerBackendType cbk(&Q); + const LogicCircuit lc(&cbk, p256k1_base); + VerifyC circuit(lc, p256k1); + + EltW rx = lc.eltw_input(); + EltW px = lc.eltw_input(); + EltW e = lc.eltw_input(); + + typename VerifyC::Witness w; + Q.private_input(); + w.input(lc); + + circuit.assert_verify(rx, px, e, w); + auto C = Q.mkcircuit(1); + dump_info("bip340 verify", Q); + + EXPECT_GT(C->npub_in, 0); + EXPECT_GT(C->ninputs, C->npub_in); +} + +// ===================== ZK Prover / Verifier ============================== + +struct Bip340TestData { + Nat s_nat; + Nat e_nat; + Elt px; + Elt py; + Elt rx; +}; + +inline Bip340TestData MakeTestData(Nat s_nat = Nat(2ull), + Nat e_nat = Nat(1ull), + Nat sk = Nat(1ull)) { + const Field& F = p256k1_base; + const EC& ec = p256k1; + auto G = ec.generator(); + auto P = ec.scalar_multf(G, sk); + ec.normalize(P); + + Nat exp("0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffff0c"); + Elt x2 = F.mulf(P.x, P.x); + Elt x3 = F.mulf(x2, P.x); + Elt y2 = F.addf(x3, ec.b_); + Elt py = F.one(); + Elt base = y2; + for (int i = 255; i >= 0; --i) { + py = F.mulf(py, py); + if (exp.bit(i)) py = F.mulf(py, base); + } + if (F.from_montgomery(py).bit(0) != 0) py = F.negf(py); + + auto sG = ec.scalar_multf(G, s_nat); + auto eP = ec.scalar_multf( + typename EC::ECPoint{P.x, py, F.one()}, e_nat); + auto neg_eP = typename EC::ECPoint{eP.x, F.negf(eP.y), eP.z}; + ec.addE(sG, neg_eP); + ec.normalize(sG); + + return {s_nat, e_nat, P.x, py, sG.x}; +} + +// ====================== Soundness Tests ================================ + +TEST(Bip340SoundnessTest, OddRYWitnessFails) { + using EvalBackend = EvaluationBackend; + using LogicType = Logic; + using VerifyC = Bip340Verify; + + const Field& F = p256k1_base; + const EC& ec = p256k1; + + auto d = MakeTestData(); + Bip340Witness wit(ec); + ASSERT_TRUE(wit.compute_from_scalars(d.s_nat, d.e_nat, d.px, d.py)); + + // Subcase 1: odd ry with consistent odd bits - fails LSB-zero check. + { + const EvalBackend ebk(F, false); + const LogicType l(&ebk, F); + VerifyC circuit(l, ec); + + auto rxx = l.konst(d.rx); + auto pxx = l.konst(d.px); + auto ee = l.konst(wit.e_); + + auto w = MakeEvalWitness(l, wit); + + // Mutate: set ry to odd (negate it) and fill bits_ry for the odd value. + Elt odd_ry = F.negf(wit.ry_); + w.ry = l.konst(odd_ry); + Nat odd_ry_nat = F.from_montgomery(odd_ry); + for (size_t i = 0; i < 256; ++i) { + w.bits_ry[i] = l.konst( + F.of_scalar(odd_ry_nat.bit(255 - i))); + } + + circuit.assert_verify(rxx, pxx, ee, w); + EXPECT_TRUE(ebk.assertion_failed()) + << "Odd ry with odd bits should fail LSB-zero check"; + } + + // Subcase 2: odd ry with original even bits - fails reconstruction. + { + const EvalBackend ebk(F, false); + const LogicType l(&ebk, F); + VerifyC circuit(l, ec); + + auto rxx = l.konst(d.rx); + auto pxx = l.konst(d.px); + auto ee = l.konst(wit.e_); + + auto w = MakeEvalWitness(l, wit); + + // Mutate: set ry to odd but keep original even bits. + w.ry = l.konst(F.negf(wit.ry_)); + + circuit.assert_verify(rxx, pxx, ee, w); + EXPECT_TRUE(ebk.assertion_failed()) + << "Odd ry with even bits should fail reconstruction"; + } +} + +TEST(Bip340SoundnessTest, RejectsScalarSAtOrAboveCurveOrder) { + using EvalBackend = EvaluationBackend; + using LogicType = Logic; + using VerifyC = Bip340Verify; + + const Field& F = p256k1_base; + const EC& ec = p256k1; + + auto d = MakeTestData(); + + // n + 2 is congruent to the valid test scalar 2 modulo the secp256k1 + // group order, so the group equation still holds. The circuit must + // reject it because BIP-340 requires the encoded scalar to satisfy s < n. + Nat s_plus_n( + "0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364143"); + Bip340Witness wit(ec); + ASSERT_TRUE(wit.compute_from_scalars(s_plus_n, d.e_nat, d.px, d.py)); + + const EvalBackend ebk(F, false); + const LogicType l(&ebk, F); + VerifyC circuit(l, ec); + + auto w = MakeEvalWitness(l, wit); + circuit.assert_verify(l.konst(d.rx), l.konst(d.px), + l.konst(wit.e_), w); + + EXPECT_TRUE(ebk.assertion_failed()) + << "Scalar s >= n should be rejected even when the group equation holds"; +} + +// ====================== Mutation Tests ================================== + +TEST(Bip340MutationTest, PrivateWitnessMutations) { + using EvalBackend = EvaluationBackend; + using LogicType = Logic; + using EltW = typename LogicType::EltW; + using VerifyC = Bip340Verify; + + const Field& F = p256k1_base; + const EC& ec = p256k1; + + auto d = MakeTestData(); + Bip340Witness wit(ec); + ASSERT_TRUE(wit.compute_from_scalars(d.s_nat, d.e_nat, d.px, d.py)); + + // Test each mutation: build eval circuit, mutate one witness field, + // assert circuit rejects. + auto run_mutation = [&](const char* name, + std::function mutate) { + const EvalBackend ebk(F, false); + const LogicType l(&ebk, F); + VerifyC circuit(l, ec); + + auto w = MakeEvalWitness(l, wit); + mutate(w, l); + + circuit.assert_verify(l.konst(d.rx), l.konst(d.px), + l.konst(wit.e_), w); + EXPECT_TRUE(ebk.assertion_failed()) + << "Mutation '" << name << "' should be rejected"; + }; + + run_mutation("flip bits_s[10]", [&](auto& w, const auto& l) { + w.bits_s[10] = l.konst(F.subf(F.one(), wit.bits_s_[10])); + }); + run_mutation("flip bits_e[20]", [&](auto& w, const auto& l) { + w.bits_e[20] = l.konst(F.subf(F.one(), wit.bits_e_[20])); + }); + // Use the last intermediate (index 254) which is non-trivial + // for typical scalar values (not the point at infinity). + run_mutation("corrupt int_sx[254]", [&](auto& w, const auto& l) { + w.int_sx[254] = l.konst(F.zero()); + }); + run_mutation("corrupt int_sy[254]", [&](auto& w, const auto& l) { + w.int_sy[254] = l.konst(F.zero()); + }); + run_mutation("corrupt int_sz[254]", [&](auto& w, const auto& l) { + w.int_sz[254] = l.konst(F.zero()); + }); + run_mutation("negate py", [&](auto& w, const auto& l) { + w.py = l.konst(F.negf(wit.py_)); + }); + run_mutation("corrupt bits_ry[0]", [&](auto& w, const auto& l) { + w.bits_ry[0] = l.konst(F.subf(F.one(), wit.bits_ry_[0])); + }); + run_mutation("set rz_inv to zero", [&](auto& w, const auto& l) { + w.rz_inv = l.konst(F.zero()); + }); + run_mutation("corrupt int_ey[254]", [&](auto& w, const auto& l) { + w.int_ey[254] = l.konst(F.zero()); + }); +} + +TEST(Bip340MutationTest, PublicInputMutations) { + using EvalBackend = EvaluationBackend; + using LogicType = Logic; + using VerifyC = Bip340Verify; + + const Field& F = p256k1_base; + const EC& ec = p256k1; + + auto d = MakeTestData(); + Bip340Witness wit(ec); + ASSERT_TRUE(wit.compute_from_scalars(d.s_nat, d.e_nat, d.px, d.py)); + + // Wrong rx. + { + const EvalBackend ebk(F, false); + const LogicType l(&ebk, F); + VerifyC circuit(l, ec); + auto w = MakeEvalWitness(l, wit); + circuit.assert_verify(l.konst(F.negf(d.rx)), l.konst(d.px), + l.konst(wit.e_), w); + EXPECT_TRUE(ebk.assertion_failed()) << "Wrong rx should be rejected"; + } + // Wrong px. + { + const EvalBackend ebk(F, false); + const LogicType l(&ebk, F); + VerifyC circuit(l, ec); + auto w = MakeEvalWitness(l, wit); + circuit.assert_verify(l.konst(d.rx), l.konst(F.negf(d.px)), + l.konst(wit.e_), w); + EXPECT_TRUE(ebk.assertion_failed()) << "Wrong px should be rejected"; + } + // Wrong e. + { + const EvalBackend ebk(F, false); + const LogicType l(&ebk, F); + VerifyC circuit(l, ec); + auto w = MakeEvalWitness(l, wit); + Elt wrong_e = F.addf(wit.e_, F.one()); + circuit.assert_verify(l.konst(d.rx), l.konst(d.px), + l.konst(wrong_e), w); + EXPECT_TRUE(ebk.assertion_failed()) << "Wrong e should be rejected"; + } +} + +class Bip340ZkTest : public ::testing::Test { + protected: + using CompilerBackendType = CompilerBackend; + using LogicCircuit = Logic; + using EltW = typename LogicCircuit::EltW; + using VerifyC = Bip340Verify; + + const Field& F = p256k1_base; + const EC& ec = p256k1; + + std::pair>, size_t> make_circuit_with_quads() { + QuadCircuit Q(F); + const CompilerBackendType cbk(&Q); + const LogicCircuit lc(&cbk, F); + VerifyC circuit(lc, ec); + + EltW rx = lc.eltw_input(); + EltW px = lc.eltw_input(); + EltW e = lc.eltw_input(); + + Q.private_input(); + typename VerifyC::Witness w; + w.input(lc); + + circuit.assert_verify(rx, px, e, w); + return {Q.mkcircuit(1), Q.nquad_terms_}; + } + + void run_zk_test(const Nat& s_nat, const Nat& e_nat, + const Elt& px, const Elt& py, const Elt& rx) { + Bip340Witness wit(ec); + ASSERT_TRUE(wit.compute_from_scalars(s_nat, e_nat, px, py)); + + auto [circuit, nquad] = make_circuit_with_quads(); + + // CRT guard: ensure block_enc fits within 2^22 FFT order. + { + using Crt = CRT256; + size_t block_enc = circuit->ninputs - circuit->npub_in + nquad + 1; + auto err = check_crt_block_enc(block_enc); + ASSERT_TRUE(err.empty()) << "CRT capacity: " << err; + } + + auto W = std::make_unique>(1, circuit->ninputs); + + // Fill prover witness. + { + DenseFiller filler(*W); + PushBip340PublicInputs(filler, F, rx, px, wit.e_); + wit.fill_witness(filler); + } + + using Crt = CRT256; + using ConvolutionFactory = CrtConvolutionFactory; + using RSFactory = ReedSolomonFactory; + + ConvolutionFactory factory(F); + RSFactory rsf(factory, F); + + Transcript tp((uint8_t*)"bip340 zk test", 14); + SecureRandomEngine rng; + + ZkProof zkpr(*circuit, kRate, kQueries); + ZkProver prover(*circuit, F, rsf); + prover.commit(zkpr, *W, tp, rng); + prover.prove(zkpr, *W, tp); + log(INFO, "BIP-340 Prover done"); + + Transcript tv((uint8_t*)"bip340 zk test", 14); + auto pub = Dense(1, circuit->npub_in); + { + DenseFiller filler(pub); + PushBip340PublicInputs(filler, F, rx, px, wit.e_); + } + + ZkVerifier verifier(*circuit, rsf, kRate, kQueries, F); + verifier.recv_commitment(zkpr, tv); + EXPECT_TRUE(verifier.verify(zkpr, pub, tv)); + log(INFO, "BIP-340 Verifier done"); + } + + Bip340TestData setup_test_data(Nat s_nat = Nat(2ull), + Nat e_nat = Nat(1ull), + Nat sk = Nat(1ull)) { + return MakeTestData(s_nat, e_nat, sk); + } +}; + +TEST_F(Bip340ZkTest, SmallScalars) { + auto d = setup_test_data(); + run_zk_test(d.s_nat, d.e_nat, d.px, d.py, d.rx); +} + +TEST_F(Bip340ZkTest, WrongPublicInputFails) { + auto d = setup_test_data(); + + // Use wrong rx (negate it). + Elt wrong_rx = F.negf(d.rx); + + Bip340Witness wit(ec); + ASSERT_TRUE(wit.compute_from_scalars(d.s_nat, d.e_nat, d.px, d.py)); + + auto circuit = make_circuit_with_quads().first; + auto W = std::make_unique>(1, circuit->ninputs); + { + DenseFiller filler(*W); + PushBip340PublicInputs(filler, F, d.rx, d.px, wit.e_); + wit.fill_witness(filler); + } + + using Crt = CRT256; + using ConvolutionFactory = CrtConvolutionFactory; + using RSFactory = ReedSolomonFactory; + + ConvolutionFactory factory(F); + RSFactory rsf(factory, F); + + Transcript tp((uint8_t*)"bip340 wrong", 12); + SecureRandomEngine rng; + + ZkProof zkpr(*circuit, kRate, kQueries); + ZkProver prover(*circuit, F, rsf); + prover.commit(zkpr, *W, tp, rng); + prover.prove(zkpr, *W, tp); + + // Verifier uses wrong rx. + Transcript tv((uint8_t*)"bip340 wrong", 12); + auto pub = Dense(1, circuit->npub_in); + { + DenseFiller filler(pub); + PushBip340PublicInputs(filler, F, wrong_rx, d.px, wit.e_); + } + + ZkVerifier verifier(*circuit, rsf, kRate, kQueries, F); + verifier.recv_commitment(zkpr, tv); + EXPECT_FALSE(verifier.verify(zkpr, pub, tv)) + << "Verification should fail with wrong public input"; +} + +TEST_F(Bip340ZkTest, LargerScalars) { + Nat s_nat("0x4a5e1bca99fee8a7c3d1f0e5b6a728394c5d6e7f8091a2b3c4d5e6f708192a3b"); + Nat e_nat("0x1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c"); + auto d = setup_test_data(s_nat, e_nat); + run_zk_test(d.s_nat, d.e_nat, d.px, d.py, d.rx); +} + +// ===================== CRT Parameter Guard ============================== + +// ===================== Randomized Test ================================== + +TEST(Bip340RandomizedTest, DeterministicSmallScalarCases) { + using EvalBackend = EvaluationBackend; + using LogicType = Logic; + using VerifyC = Bip340Verify; + + const Field& F = p256k1_base; + const EC& ec = p256k1; + constexpr size_t kNumCases = 32; + + // Deterministic seed: small consecutive values, not randomness. + size_t collected = 0; + for (uint64_t sk = 1; sk < 300 && collected < kNumCases; ++sk) { + for (uint64_t s = 1; s < 300 && collected < kNumCases; ++s) { + for (uint64_t e = 1; e < 20 && collected < kNumCases; ++e) { + Nat sk_nat(sk), s_nat(s), e_nat(e); + auto G = ec.generator(); + auto P = ec.scalar_multf(G, sk_nat); + ec.normalize(P); + + // Pick even py. + Nat exp_n("0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffff0c"); + Elt x2 = F.mulf(P.x, P.x); + Elt x3 = F.mulf(x2, P.x); + Elt y2 = F.addf(x3, ec.b_); + Elt py = F.one(), base = y2; + for (int i = 255; i >= 0; --i) { py = F.mulf(py, py); if (exp_n.bit(i)) py = F.mulf(py, base); } + if (F.from_montgomery(py).bit(0) != 0) py = F.negf(py); + if (F.mulf(py, py) != y2) continue; // P.x not liftable + + // Compute R from the witness's own intermediates (same algorithm + // as the circuit's double-and-add, so projective coords match). + Bip340Witness wit(ec); + ASSERT_TRUE(wit.compute_from_scalars(s_nat, e_nat, P.x, py)); + + // Extract final sG and eP from witness intermediates. + typename EC::ECPoint sG_pt = { + wit.int_sx_[Bip340Witness::kBits - 1], + wit.int_sy_[Bip340Witness::kBits - 1], + wit.int_sz_[Bip340Witness::kBits - 1]}; + typename EC::ECPoint eP_pt = { + wit.int_ex_[Bip340Witness::kBits - 1], + wit.int_ey_[Bip340Witness::kBits - 1], + wit.int_ez_[Bip340Witness::kBits - 1]}; + + // R = sG - eP. + auto neg_ep = typename EC::ECPoint{eP_pt.x, F.negf(eP_pt.y), eP_pt.z}; + ec.addE(sG_pt, neg_ep); + ec.normalize(sG_pt); + + // Skip cases with odd R.y. + if (F.from_montgomery(sG_pt.y).bit(0) != 0) continue; + Elt rx = sG_pt.x; + + const EvalBackend ebk(F, true); + const LogicType l(&ebk, F); + VerifyC circuit(l, ec); + auto w = MakeEvalWitness(l, wit); + circuit.assert_verify(l.konst(rx), l.konst(P.x), l.konst(wit.e_), w); + ASSERT_FALSE(ebk.assertion_failed()) + << "sk=" << sk << " s=" << s << " e=" << e; + ++collected; + } + } + } + log(INFO, "Randomized test: %zu even-R cases collected", collected); + EXPECT_GE(collected, kNumCases); +} + +// ===================== Proof Tamper Test ================================= + +TEST(Bip340TamperTest, TamperedProofFailsVerification) { + set_log_level(INFO); + const Field& F = p256k1_base; + const EC& ec = p256k1; + + const auto& tv = kRealVectors[0]; + auto pk = hex_vec(tv.pk_hex); + auto msg = hex_vec(tv.msg_hex); + auto sig = hex_vec(tv.sig_hex); + + Bip340Witness wit(ec); + ASSERT_TRUE(wit.compute(sig.data(), pk.data(), msg.data(), msg.size())); + + using CompilerBackendType = CompilerBackend; + using LogicCircuit = Logic; + using VerifyCC = Bip340Verify; + + QuadCircuit Q(F); + const CompilerBackendType cbk(&Q); + const LogicCircuit lc(&cbk, F); + VerifyCC circuit(lc, ec); + + auto rxx = lc.eltw_input(); + auto pxx = lc.eltw_input(); + auto ee = lc.eltw_input(); + Q.private_input(); + typename VerifyCC::Witness w; + w.input(lc); + circuit.assert_verify(rxx, pxx, ee, w); + auto CIRCUIT = Q.mkcircuit(1); + + // CRT guard. + { + using Crt = CRT256; + size_t block_enc = CIRCUIT->ninputs - CIRCUIT->npub_in + + Q.nquad_terms_ + 1; + auto err = check_crt_block_enc(block_enc); + ASSERT_TRUE(err.empty()) << "CRT capacity: " << err; + } + + auto W = std::make_unique>(1, CIRCUIT->ninputs); + { + DenseFiller filler(*W); + PushBip340PublicInputs(filler, F, + F.to_montgomery(Bip340Witness::nat_from_be_bytes(sig.data())), + F.to_montgomery(Bip340Witness::nat_from_be_bytes(pk.data())), + wit.e_); + wit.fill_witness(filler); + } + + using Crt = CRT256; + using ConvolutionFactory = CrtConvolutionFactory; + using RSFactory = ReedSolomonFactory; + + ConvolutionFactory factory(F); + RSFactory rsf(factory, F); + + Transcript tp((uint8_t*)"bip340 tamper", 13); + SecureRandomEngine rng; + + ZkProof zkpr(*CIRCUIT, kRate, kQueries); + ZkProver prover(*CIRCUIT, F, rsf); + prover.commit(zkpr, *W, tp, rng); + prover.prove(zkpr, *W, tp); + + // Serialize to bytes, corrupt one byte, read back. + std::vector buf; + zkpr.write(buf, F); + ASSERT_GE(buf.size(), 20u) << "Proof too small to tamper"; + buf[10] ^= 0xFF; // flip all bits of byte 10 + + ReadBuffer rbuf(buf); + ZkProof zkpr_tampered(*CIRCUIT, kRate, kQueries); + ASSERT_TRUE(zkpr_tampered.read(rbuf, F)) << "Failed to read tampered proof"; + + // Verification with tampered proof must fail. + Transcript tv2((uint8_t*)"bip340 tamper", 13); + auto pub = Dense(1, CIRCUIT->npub_in); + { + DenseFiller filler(pub); + PushBip340PublicInputs(filler, F, + F.to_montgomery(Bip340Witness::nat_from_be_bytes(sig.data())), + F.to_montgomery(Bip340Witness::nat_from_be_bytes(pk.data())), + wit.e_); + } + + ZkVerifier verifier(*CIRCUIT, rsf, kRate, kQueries, F); + verifier.recv_commitment(zkpr_tampered, tv2); + EXPECT_FALSE(verifier.verify(zkpr_tampered, pub, tv2)) + << "Tampered proof should fail verification"; +} + +// ===================== CRT Parameter Guard ============================== + +TEST(Bip340GuardTest, AcceptsReasonableBlockEnc) { + using Crt = CRT256; + // block_enc = 1024 -> padding = 1024 < 2^22. + EXPECT_TRUE(check_crt_block_enc(1024).empty()); + // block_enc = 2^22 -> padding = 2^22, exactly at limit. + EXPECT_TRUE(check_crt_block_enc(1ull << 22).empty()); +} + +TEST(Bip340GuardTest, RejectsExcessiveBlockEnc) { + using Crt = CRT256; + // block_enc = 2^22 + 1 -> padding = 2^23, exceeds 2^22. + auto err = check_crt_block_enc((1ull << 22) + 1); + EXPECT_FALSE(err.empty()); + EXPECT_NE(err.find("exceeds"), std::string::npos); +} + +// ===================== Parameter Measurement ============================ + +TEST(Bip340ParamTest, ReportCircuitParams) { + using CompilerBackendType = CompilerBackend; + using LogicCircuit = Logic; + using EltW = typename LogicCircuit::EltW; + using VerifyC = Bip340Verify; + + QuadCircuit Q(p256k1_base); + const CompilerBackendType cbk(&Q); + const LogicCircuit lc(&cbk, p256k1_base); + VerifyC circuit(lc, p256k1); + + EltW rx = lc.eltw_input(); + EltW px = lc.eltw_input(); + EltW e = lc.eltw_input(); + + typename VerifyC::Witness w; + Q.private_input(); + w.input(lc); + + circuit.assert_verify(rx, px, e, w); + auto C = Q.mkcircuit(1); + + // Detailed parameter report. + size_t nwires = Q.nwires_; + size_t nquad = Q.nquad_terms_; + size_t depth = Q.depth_; + size_t nin = Q.ninput_; + size_t npriv = C->ninputs - C->npub_in; + size_t npub = C->npub_in; + + log(INFO, "BIP-340 Circuit Parameters:"); + log(INFO, " wires(total)=%zu", nwires); + log(INFO, " quad_terms=%zu", nquad); + log(INFO, " depth=%zu", depth); + log(INFO, " inputs(total)=%zu", nin); + log(INFO, " public_inputs=%zu", npub); + log(INFO, " private_inputs=%zu", npriv); + + // The circuit should produce non-trivial parameters. + EXPECT_GT(nwires, 1000); + EXPECT_GT(nquad, 100); + EXPECT_GT(depth, 0u); + + // Check LigeroParam-like derived values. + // nw = number of witness elements ; nq = quadr constraints. + // block_enc = (nw + nq + 1) rounded up for RS encoding. + size_t nw_approx = npriv; + size_t nq_approx = nquad; + size_t block_enc_approx = nw_approx + nq_approx + 1; + size_t pad = next_pow2(block_enc_approx); + + log(INFO, " estimated block_enc=%zu, padding=%zu", block_enc_approx, pad); + + using Crt = CRT256; + auto err = check_crt_block_enc(block_enc_approx); + EXPECT_TRUE(err.empty()) + << "BIP-340 circuit block_enc exceeds CRT capacity: " << err; +} + +// ===================== Scale Smoke Test ================================= + +TEST(Bip340ScaleTest, MultiInstanceProof) { + // Run a ZK proof with 2 instances to verify scaling works. + set_log_level(INFO); + + using CompilerBackendType = CompilerBackend; + using LogicCircuit = Logic; + using EltW = typename LogicCircuit::EltW; + using VerifyC = Bip340Verify; + + constexpr size_t kNumInstances = 2; + + // Build circuit with multiple BIP-340 verification instances. + QuadCircuit Q(p256k1_base); + const CompilerBackendType cbk(&Q); + const LogicCircuit lc(&cbk, p256k1_base); + VerifyC circuit(lc, p256k1); + + std::vector ws(kNumInstances); + std::vector rxs(kNumInstances); + std::vector pxs(kNumInstances); + std::vector es(kNumInstances); + + for (size_t i = 0; i < kNumInstances; ++i) { + rxs[i] = lc.eltw_input(); + pxs[i] = lc.eltw_input(); + es[i] = lc.eltw_input(); + } + Q.private_input(); + for (size_t i = 0; i < kNumInstances; ++i) { + ws[i].input(lc); + } + for (size_t i = 0; i < kNumInstances; ++i) { + circuit.assert_verify(rxs[i], pxs[i], es[i], ws[i]); + } + auto CIRCUIT = Q.mkcircuit(1); + + dump_info("bip340 multi", Q); + + // Check CRT capacity. + using Crt = CRT256; + size_t block_enc_approx = CIRCUIT->ninputs - CIRCUIT->npub_in + + Q.nquad_terms_ + 1; + auto err = check_crt_block_enc(block_enc_approx); + ASSERT_TRUE(err.empty()) << "Scale test exceeds CRT capacity: " << err; + + // Prepare witness data. + auto d = MakeTestData(); + + Bip340Witness wit(p256k1); + ASSERT_TRUE(wit.compute_from_scalars(d.s_nat, d.e_nat, d.px, d.py)); + + auto W = std::make_unique>(1, CIRCUIT->ninputs); + { + DenseFiller filler(*W); + filler.push_back(p256k1_base.one()); + for (size_t i = 0; i < kNumInstances; ++i) { + filler.push_back(d.rx); + filler.push_back(d.px); + filler.push_back(wit.e_); + } + for (size_t i = 0; i < kNumInstances; ++i) { + wit.fill_witness(filler); + } + } + + using ConvolutionFactory = CrtConvolutionFactory; + using RSFactory = ReedSolomonFactory; + + ConvolutionFactory factory(p256k1_base); + RSFactory rsf(factory, p256k1_base); + + Transcript tp((uint8_t*)"bip340 scale", 12); + SecureRandomEngine rng; + + ZkProof zkpr(*CIRCUIT, kRate, kQueries); + ZkProver prover(*CIRCUIT, p256k1_base, rsf); + prover.commit(zkpr, *W, tp, rng); + prover.prove(zkpr, *W, tp); + log(INFO, "BIP-340 scale prover done (%zu instances)", kNumInstances); + + // Verifier. + Transcript tv((uint8_t*)"bip340 scale", 12); + auto pub = Dense(1, CIRCUIT->npub_in); + { + DenseFiller filler(pub); + filler.push_back(p256k1_base.one()); + for (size_t i = 0; i < kNumInstances; ++i) { + filler.push_back(d.rx); + filler.push_back(d.px); + filler.push_back(wit.e_); + } + } + + ZkVerifier verifier(*CIRCUIT, rsf, kRate, kQueries, + p256k1_base); + verifier.recv_commitment(zkpr, tv); + EXPECT_TRUE(verifier.verify(zkpr, pub, tv)); + log(INFO, "BIP-340 scale verifier done"); +} + +} // namespace +} // namespace proofs diff --git a/lib/circuits/tests/contrib/bip340/bip340_verify.h b/lib/circuits/tests/contrib/bip340/bip340_verify.h new file mode 100644 index 00000000..31a65d5a --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/bip340_verify.h @@ -0,0 +1,363 @@ +// Copyright 2026 Google LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef PRIVACY_PROOFS_ZK_LIB_CIRCUITS_TESTS_CONTRIB_BIP340_BIP340_VERIFY_H_ +#define PRIVACY_PROOFS_ZK_LIB_CIRCUITS_TESTS_CONTRIB_BIP340_BIP340_VERIFY_H_ + +#include + +namespace proofs { + +/// Production BIP-340 / Schnorr signature verification over secp256k1. +/// +/// s*G = R + e*P +/// +/// --- In-circuit (proven) --- +/// +/// Public field inputs, in order: rx, px, e. +/// rx : R.x (x-only, 32 bytes after BIP-340 parse). +/// px : P.x (x-only public key). +/// e : Fiat-Shamir challenge scalar (field element). +/// +/// Private witness: bits_s[256], int_s_{x,y,z}[255] for s*G; +/// bits_e[256], int_e_{x,y,z}[255] for e*P; +/// py (P.y, the even square root); +/// ry (affine R.y); rz_inv (R.z inverse); ry_bits[256]. +/// +/// Circuit constraints: +/// - e is reconstructed from bits_e[256] (MSB-first). +/// - s is range-checked as a scalar: 0 <= s < n. +/// - py^2 = px^3 + 7 (P is on the secp256k1 curve). +/// - ry^2 = rx^3 + 7 (R is on the secp256k1 curve). +/// - Double-and-add trace for s*G with intermediate witnesses. +/// - Double-and-add trace for e*P with intermediate witnesses. +/// - R = s*G - e*P computed in projective coordinates. +/// - R.z * rz_inv = 1 (R is not the point at infinity). +/// - R.x = rx (projective equality: R.x * 1 == rx * R.z). +/// - R.y = ry (projective equality; ry is the affine y). +/// - ry is canonically even: ry_bits[255] (LSB) = 0, and +/// ry reconstructed from ry_bits[256] matches ry. +/// - Each ry_bits[i] in {0,1}. +/// +/// --- Outside the circuit (witness/verifier validation) --- +/// +/// Witness generation checks before building the circuit: +/// - Byte-length validation: sig 64 bytes, pk 32 bytes. +/// - rx < p, s < n, px < p. +/// - px is liftable (curve point exists with even y). +/// - e is computed from BIP-340 tagged SHA-256 hash. +/// +/// Tagged SHA-256 is deliberately NOT proven in this circuit. +/// The circuit proves the algebraic BIP-340 relation given a +/// public challenge value e. The binding between e and the +/// message / public key is established by the verifier's own +/// hash computation outside the proof system. +template +class Bip340Verify { + using EltW = typename LogicCircuit::EltW; + using Elt = typename LogicCircuit::Elt; + using Nat = typename Field::N; + using Bitvec = typename LogicCircuit::v256; + static constexpr size_t kBits = EC::kBits; + + public: + struct Witness { + EltW bits_s[kBits]; + EltW int_sx[kBits]; + EltW int_sy[kBits]; + EltW int_sz[kBits]; + + EltW bits_e[kBits]; + EltW int_ex[kBits]; + EltW int_ey[kBits]; + EltW int_ez[kBits]; + + EltW py; // affine P.y (the even square root) + EltW ry; // affine R.y (witnessed even value) + EltW rz_inv; // inverse of R.z (proves R finite) + EltW bits_ry[kBits]; // affine ry bits, MSB-first + + void input(const LogicCircuit& lc) { + for (size_t i = 0; i < kBits; ++i) { + bits_s[i] = lc.eltw_input(); + if (i < kBits - 1) { + int_sx[i] = lc.eltw_input(); + int_sy[i] = lc.eltw_input(); + int_sz[i] = lc.eltw_input(); + } + } + for (size_t i = 0; i < kBits; ++i) { + bits_e[i] = lc.eltw_input(); + if (i < kBits - 1) { + int_ex[i] = lc.eltw_input(); + int_ey[i] = lc.eltw_input(); + int_ez[i] = lc.eltw_input(); + } + } + py = lc.eltw_input(); + ry = lc.eltw_input(); + rz_inv = lc.eltw_input(); + for (size_t i = 0; i < kBits; ++i) { + bits_ry[i] = lc.eltw_input(); + } + } + }; + + Bip340Verify(const LogicCircuit& lc, const EC& ec) : lc_(lc), ec_(ec) { + Nat order( + "0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + for (size_t i = 0; i < kBits; ++i) { + bits_n_[i] = lc_.bit(order.bit(i)); + } + } + + /// Verify the BIP-340 relation: s*G - e*P = R, with R.x == rx. + /// + /// rx: x-coordinate of R (public, x-only) + /// px: x-coordinate of P (public, x-only public key) + /// e: Fiat-Shamir challenge (public scalar, field element) + /// w: witness containing bits of s and e, intermediate points, and py + void assert_verify(EltW rx, EltW px, EltW e, const Witness& w) const { + EltW zero = lc_.konst(lc_.zero()); + EltW one = lc_.konst(lc_.one()); + + // -- 0. Verify e matches bits_e decomposition ------------------------ + // e must equal sum_i bits_e[i] * 2^(kBits-1-i), i.e., the scalar + // represented by the bits in MSB-first order. + { + EltW check = lc_.konst(lc_.zero()); + EltW pow = lc_.konst(lc_.one()); // 2^0 + for (int i = static_cast(kBits) - 1; i >= 0; --i) { + check = lc_.add(check, lc_.mul(w.bits_e[i], pow)); + pow = lc_.add(pow, pow); // pow *= 2 + } + lc_.assert_eq(check, e); + } + + // -- 1. Verify s is a canonical secp256k1 scalar --------------------- + // bits_s is MSB-first for scalar_mult(), while Logic::vlt expects + // bit-vectors with the least-significant bit at index 0. + { + Bitvec bits_s; + for (size_t i = 0; i < kBits; ++i) { + bits_s[kBits - 1 - i] = + typename LogicCircuit::BitW(w.bits_s[i], lc_.f_); + } + lc_.assert1(lc_.vlt(bits_s, bits_n_)); + } + + // -- 2. Lift P: verify py^2 = px^3 + b (secp256k1: b = 7) -------------- + assert_point_on_curve(px, w.py); + + // -- 3. Compute s*G --------------------------------------------------- + EltW gx = lc_.konst(ec_.gx_); + EltW gy = lc_.konst(ec_.gy_); + EltW sgx = zero, sgy = one, sgz = zero; + scalar_mult(sgx, sgy, sgz, gx, gy, one, w.bits_s, w.int_sx, w.int_sy, + w.int_sz); + + // -- 4. Compute e*P (P = (px, py, 1)) ------------------------------- + EltW epx = zero, epy = one, epz = zero; + scalar_mult(epx, epy, epz, px, w.py, one, w.bits_e, w.int_ex, w.int_ey, + w.int_ez); + + // -- 5. Compute R = sG - eP = sG + (-eP) ------------------------------ + EltW neg_epy = lc_.sub(zero, epy); + EltW rpx, rpy, rpz; + addE(rpx, rpy, rpz, sgx, sgy, sgz, epx, neg_epy, epz); + + // -- 6. Verify R is on the curve and finite -------------------------- + assert_point_on_curve(rx, w.ry); + + // R.z * rz_inv = 1 <=> R is not the point at infinity. + lc_.assert_eq(lc_.mul(rpz, w.rz_inv), one); + + // -- 7. Check R.x == rx (projective) --------------------------------- + lc_.assert_eq(rpx, lc_.mul(rx, rpz)); // R.x * 1 == rx * R.z + + // -- 8. Check R.y == ry (projective) --------------------------------- + lc_.assert_eq(rpy, lc_.mul(w.ry, rpz)); // R.y * 1 == ry * R.z + + // -- 9. Verify ry bitness and even parity ---------------------------- + // bits_ry[0] is MSB, bits_ry[kBits-1] is LSB. + EltW ry_check = lc_.konst(lc_.zero()); + for (size_t i = 0; i < kBits; ++i) { + typename LogicCircuit::BitW b_bit(w.bits_ry[i], lc_.f_); + lc_.assert_is_bit(b_bit); + ry_check = lc_.add(ry_check, ry_check); // ry_check *= 2 + ry_check = lc_.add(ry_check, w.bits_ry[i]); + } + lc_.assert_eq(ry_check, w.ry); + + // Assert LSB is zero (bits_ry[255] in MSB-first order). + lc_.assert_eq(w.bits_ry[kBits - 1], zero); + } + + private: + /// Verify that (x, y) is on the curve: y^2 = x^3 + a*x + b. + /// For secp256k1, a = 0, b = 7. + void assert_point_on_curve(EltW x, EltW y) const { + auto y2 = lc_.mul(y, y); + auto x2 = lc_.mul(x, x); + auto x3 = lc_.mul(x, x2); + auto ax = lc_.mul(lc_.konst(ec_.a_), x); + auto b = lc_.konst(ec_.b_); + auto rhs = lc_.add(lc_.add(x3, ax), b); + lc_.assert_eq(y2, rhs); + } + + /// Double-and-add scalar multiplication with witnessed intermediate + /// points. Same pattern as Ecpk::assert_public_key. + void scalar_mult(EltW& rx, EltW& ry, EltW& rz, EltW px, EltW py, EltW pz, + const EltW bits[kBits], const EltW int_x[kBits], + const EltW int_y[kBits], + const EltW int_z[kBits]) const { + EltW zero = lc_.konst(lc_.zero()); + EltW one = lc_.konst(lc_.one()); + + // Accumulator starts at point at infinity (0, 1, 0). + EltW ax = zero, ay = one, az = zero; + + for (size_t i = 0; i < kBits; ++i) { + typename LogicCircuit::BitW b_bit(bits[i], lc_.f_); + lc_.assert_is_bit(b_bit); + + // Select point to add: if bit == 1 -> P, else -> infinity. + EltW tx = lc_.mux(b_bit, px, zero); + EltW ty = lc_.mux(b_bit, py, one); + EltW tz = lc_.mux(b_bit, pz, zero); + + // Double accumulator. + doubleE(ax, ay, az, ax, ay, az); + + // Add selected point. + addE(ax, ay, az, ax, ay, az, tx, ty, tz); + + // Check against intermediate witness (except last iteration). + if (i < kBits - 1) { + lc_.assert_eq(ax, int_x[i]); + lc_.assert_eq(ay, int_y[i]); + lc_.assert_eq(az, int_z[i]); + + ax = int_x[i]; + ay = int_y[i]; + az = int_z[i]; + } + } + + rx = ax; + ry = ay; + rz = az; + } + + // -- Elliptic curve group law (projective, complete) -------------------- + + void addE(EltW& X3, EltW& Y3, EltW& Z3, EltW X1, EltW Y1, EltW Z1, EltW X2, + EltW Y2, EltW Z2) const { + EltW t0 = lc_.mul(X1, X2); + EltW t1 = lc_.mul(Y1, Y2); + EltW t2 = lc_.mul(Z1, Z2); + EltW t3 = lc_.add(X1, Y1); + EltW t4 = lc_.add(X2, Y2); + t3 = lc_.mul(t3, t4); + t4 = lc_.add(t0, t1); + t3 = lc_.sub(t3, t4); + t4 = lc_.add(X1, Z1); + EltW t5 = lc_.add(X2, Z2); + t4 = lc_.mul(t4, t5); + t5 = lc_.add(t0, t2); + t4 = lc_.sub(t4, t5); + t5 = lc_.add(Y1, Z1); + EltW X3t = lc_.add(Y2, Z2); + t5 = lc_.mul(t5, X3t); + X3t = lc_.add(t1, t2); + t5 = lc_.sub(t5, X3t); + auto a = lc_.konst(ec_.a_); + EltW Z3t = lc_.mul(a, t4); + auto k3b = lc_.konst(ec_.k3b); + X3t = lc_.mul(k3b, t2); + Z3t = lc_.add(X3t, Z3t); + X3t = lc_.sub(t1, Z3t); + Z3t = lc_.add(t1, Z3t); + EltW Y3t = lc_.mul(X3t, Z3t); + t1 = lc_.add(t0, t0); + t1 = lc_.add(t1, t0); + t2 = lc_.mul(a, t2); + t4 = lc_.mul(k3b, t4); + t1 = lc_.add(t1, t2); + t2 = lc_.sub(t0, t2); + t2 = lc_.mul(a, t2); + t4 = lc_.add(t4, t2); + t0 = lc_.mul(t1, t4); + Y3t = lc_.add(Y3t, t0); + t0 = lc_.mul(t5, t4); + X3t = lc_.mul(t3, X3t); + X3t = lc_.sub(X3t, t0); + t0 = lc_.mul(t3, t1); + Z3t = lc_.mul(t5, Z3t); + Z3t = lc_.add(Z3t, t0); + + X3 = X3t; + Y3 = Y3t; + Z3 = Z3t; + } + + void doubleE(EltW& X3, EltW& Y3, EltW& Z3, EltW X, EltW Y, EltW Z) const { + EltW t0 = lc_.mul(X, X); + EltW t1 = lc_.mul(Y, Y); + EltW t2 = lc_.mul(Z, Z); + EltW t3 = lc_.mul(X, Y); + t3 = lc_.add(t3, t3); + EltW Z3t = lc_.mul(X, Z); + Z3t = lc_.add(Z3t, Z3t); + auto a = lc_.konst(ec_.a_); + auto k3b = lc_.konst(ec_.k3b); + EltW X3t = lc_.mul(a, Z3t); + EltW Y3t = lc_.mul(k3b, t2); + Y3t = lc_.add(X3t, Y3t); + X3t = lc_.sub(t1, Y3t); + Y3t = lc_.add(t1, Y3t); + Y3t = lc_.mul(X3t, Y3t); + X3t = lc_.mul(t3, X3t); + Z3t = lc_.mul(k3b, Z3t); + t2 = lc_.mul(a, t2); + t3 = lc_.sub(t0, t2); + t3 = lc_.mul(a, t3); + t3 = lc_.add(t3, Z3t); + Z3t = lc_.add(t0, t0); + t0 = lc_.add(Z3t, t0); + t0 = lc_.add(t0, t2); + t0 = lc_.mul(t0, t3); + Y3t = lc_.add(Y3t, t0); + t2 = lc_.mul(Y, Z); + t2 = lc_.add(t2, t2); + t0 = lc_.mul(t2, t3); + X3t = lc_.sub(X3t, t0); + Z3t = lc_.mul(t2, t1); + Z3t = lc_.add(Z3t, Z3t); + Z3t = lc_.add(Z3t, Z3t); + + X3 = X3t; + Y3 = Y3t; + Z3 = Z3t; + } + + const LogicCircuit& lc_; + const EC& ec_; + Bitvec bits_n_; +}; + +} // namespace proofs + +#endif // PRIVACY_PROOFS_ZK_LIB_CIRCUITS_TESTS_CONTRIB_BIP340_BIP340_VERIFY_H_ diff --git a/lib/circuits/tests/contrib/bip340/bip340_witness.h b/lib/circuits/tests/contrib/bip340/bip340_witness.h new file mode 100644 index 00000000..e3a4564c --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/bip340_witness.h @@ -0,0 +1,348 @@ +// Copyright 2026 Google LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef PRIVACY_PROOFS_ZK_LIB_CIRCUITS_TESTS_CONTRIB_BIP340_BIP340_WITNESS_H_ +#define PRIVACY_PROOFS_ZK_LIB_CIRCUITS_TESTS_CONTRIB_BIP340_BIP340_WITNESS_H_ + +#include +#include +#include +#include + +#include "arrays/dense.h" +#include "ec/p256k1.h" + +// OpenSSL for SHA-256 (already a project dependency). +#include + +namespace proofs { + +/// Witness generator for the Bip340Verify production circuit. +/// +/// Holds field elements for the private witness, computed either from +/// raw BIP-340 signature bytes or directly from known scalars (for +/// testing). The witness layout matches Bip340Verify::Witness::input(). +/// +/// Fields (in witness input order): +/// bits_s[256], int_sx[255], int_sy[255], int_sz[255] - s*G trace +/// bits_e[256], int_ex[255], int_ey[255], int_ez[255] - e*P trace +/// py - affine P.y (the even square root of px^3+7) +/// ry - affine R.y (computed from s*G - e*P) +/// rz_inv - inverse of R.z (proves R != point-at-infinity) +/// bits_ry[256] - bits of affine ry, MSB-first, allowing the circuit +/// to reconstruct ry and enforce even parity (LSB=0). +/// +/// Validation performed by compute() (NOT proven in-circuit): +/// - rx < p, s < n, px < p (structural) +/// - px is liftable (even-y curve point exists) +/// - e is computed from BIP-340 tagged SHA-256 +class Bip340Witness { + using Field = Fp256k1Base; + using Elt = typename Field::Elt; + using Nat = typename Field::N; + using EC = P256k1; + + public: + static constexpr size_t kBits = EC::kBits; // 256 + + const EC& ec_; + + // s-bit decomposition and s*G intermediate points. + Elt bits_s_[kBits]; + Elt int_sx_[kBits]; + Elt int_sy_[kBits]; + Elt int_sz_[kBits]; + + // e-bit decomposition and e*P intermediate points. + Elt bits_e_[kBits]; + Elt int_ex_[kBits]; + Elt int_ey_[kBits]; + Elt int_ez_[kBits]; + + // P.y (the even square root of px^3 + b). + Elt py_; + + // R.y (affine, the canonical even y-coordinate of R), its inverse z, + // and its 256-bit decomposition. + Elt ry_; + Elt rz_inv_; + Elt bits_ry_[kBits]; + + // Challenge e as a base-field element (Montgomery form). + Elt e_; + // Challenge e as a Nat (for native verification). + Nat e_nat_; + + explicit Bip340Witness(const EC& ec) : ec_(ec) {} + + /// Compute witness directly from known scalars (s, e) and key material. + /// This avoids BIP-340 signature parsing; useful for circuit testing. + bool compute_from_scalars(const Nat& s_nat, const Nat& e_nat, + const Elt& px, const Elt& py) { + const Field& F = ec_.f_; + e_ = F.to_montgomery(e_nat); + e_nat_ = e_nat; + py_ = py; + + auto G = ec_.generator(); + compute_scalar_mult_witness(bits_s_, int_sx_, int_sy_, int_sz_, G, + s_nat); + + typename EC::ECPoint P = {px, py, F.one()}; + compute_scalar_mult_witness(bits_e_, int_ex_, int_ey_, int_ez_, P, + e_nat); + + // Compute R = sG - eP and derive ry, rz_inv, bits_ry. + compute_ry_witness(); + return true; + } + + /// Compute the full witness from signature and key bytes. + /// + /// sig_bytes: 64-byte BIP-340 signature (r[0:32] || s[32:64]) + /// pk_bytes: 32-byte x-only public key + /// msg: message bytes + /// msg_len: length of message in bytes + /// + /// Returns true on success, false if inputs are invalid per BIP-340 + /// (r >= p, s >= n, pk x >= p, pk not on the curve). + bool compute(const uint8_t sig_bytes[64], const uint8_t pk_bytes[32], + const uint8_t* msg, size_t msg_len) { + const Field& F = ec_.f_; + const Elt one = F.one(); + const Elt zero = F.zero(); + + // -- Parse r, s, P.x (BIP-340 uses big-endian) ---------------------- + Nat rx_nat = nat_from_be_bytes(sig_bytes); + Nat s_nat = nat_from_be_bytes(sig_bytes + 32); + Nat px_nat = nat_from_be_bytes(pk_bytes); + + // -- Validate r < p, s < n, px < p (BIP-340 requirements) ---------- + Nat p_order( + "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"); + Nat n_order = scalar_order_nat(); + if (!(rx_nat < p_order)) return false; + if (!(s_nat < n_order)) return false; + if (!(px_nat < p_order)) return false; + + // Convert to Montgomery form. + Elt px = F.to_montgomery(px_nat); + + // -- Lift P.x: compute py = sqrt(px^3 + 7), choosing the even root ---- + Elt x2 = F.mulf(px, px); + Elt x3 = F.mulf(x2, px); + Elt y2 = F.addf(x3, ec_.b_); // b = 7 + py_ = sqrt_even(y2, F); + + // Verify py^2 == y2 (pk is on the curve). + if (F.mulf(py_, py_) != y2) return false; + + // -- Compute challenge e = tagged_hash("BIP0340/challenge", + // R.x || P.x || msg) mod n --------------------------------------- + uint8_t hash[32]; + compute_tagged_hash(hash, sig_bytes, pk_bytes, msg, msg_len); + Nat e_nat = nat_from_be_bytes(hash); + + // Reduce mod n if needed. + if (!(e_nat < n_order)) { + e_nat.sub(n_order); + } + + // Store challenge as field element and Nat. + e_ = F.to_montgomery(e_nat); + e_nat_ = e_nat; + + // -- Compute s*G witness --------------------------------------------- + auto G = ec_.generator(); + compute_scalar_mult_witness(bits_s_, int_sx_, int_sy_, int_sz_, G, + s_nat); + + // -- Compute e*P witness --------------------------------------------- + typename EC::ECPoint P = {px, py_, one}; + compute_scalar_mult_witness(bits_e_, int_ex_, int_ey_, int_ez_, P, + e_nat); + + // -- Compute R = sG - eP, derive ry, rz_inv, bits_ry ---------------- + compute_ry_witness(); + + return true; + } + + /// Convert 32 big-endian bytes to a Nat (BIP-340 uses big-endian). + static Nat nat_from_be_bytes(const uint8_t bytes[32]) { + uint8_t le[32]; + for (int i = 0; i < 32; ++i) { + le[i] = bytes[31 - i]; + } + return Nat::of_bytes(le, 256); + } + + /// Fill a Dense array from this witness (for the prover). + void fill_witness(DenseFiller& filler) const { + // s*G: bits + intermediates (all but last for intermediates) + for (size_t i = 0; i < kBits; ++i) { + filler.push_back(bits_s_[i]); + if (i < kBits - 1) { + filler.push_back(int_sx_[i]); + filler.push_back(int_sy_[i]); + filler.push_back(int_sz_[i]); + } + } + // e*P: bits + intermediates (all but last for intermediates) + for (size_t i = 0; i < kBits; ++i) { + filler.push_back(bits_e_[i]); + if (i < kBits - 1) { + filler.push_back(int_ex_[i]); + filler.push_back(int_ey_[i]); + filler.push_back(int_ez_[i]); + } + } + // P.y + filler.push_back(py_); + // R.y (affine), rz_inv, bits_ry + filler.push_back(ry_); + filler.push_back(rz_inv_); + for (size_t i = 0; i < kBits; ++i) { + filler.push_back(bits_ry_[i]); + } + } + + private: + /// Fill bits[kBits] from a Nat value, MSB-first. + void fill_bits(Elt bits[kBits], const Nat& value) const { + const Field& F = ec_.f_; + for (size_t i = 0; i < kBits; ++i) { + size_t bit_idx = kBits - 1 - i; // MSB first + bits[i] = F.of_scalar(value.bit(bit_idx)); + } + } + + /// Compute R = s*G - e*P from the already-computed intermediate + /// points, derive ry_, rz_inv_, and bits_ry_. The intermediate + /// points were produced by compute_scalar_mult_witness using + /// the same double-and-add formulas as the circuit, so projective + /// coordinates match. + void compute_ry_witness() { + const Field& F = ec_.f_; + + // sG and eP are in int_s*_[kBits-1] and int_e*_[kBits-1]. + typename EC::ECPoint sG_pt = {int_sx_[kBits-1], int_sy_[kBits-1], + int_sz_[kBits-1]}; + typename EC::ECPoint eP_pt = {int_ex_[kBits-1], int_ey_[kBits-1], + int_ez_[kBits-1]}; + + // R = sG - eP. + auto neg_eP = typename EC::ECPoint{eP_pt.x, F.negf(eP_pt.y), eP_pt.z}; + ec_.addE(sG_pt, neg_eP); + + // rz_inv: inverse of projective R.z. + rz_inv_ = F.invertf(sG_pt.z); + + // Normalize to get affine ry. + Elt rz_inv_y = F.invertf(sG_pt.z); + ry_ = F.mulf(sG_pt.y, rz_inv_y); + + // Decompose ry into bits, MSB-first. + Nat ry_nat = F.from_montgomery(ry_); + fill_bits(bits_ry_, ry_nat); + } + /// BIP-340 tagged hash: SHA256(SHA256(tag) || SHA256(tag) || R.x || P.x || msg). + static void compute_tagged_hash(uint8_t hash[32], + const uint8_t r_bytes[32], + const uint8_t pk_bytes[32], + const uint8_t* msg, size_t msg_len) { + const char tag[] = "BIP0340/challenge"; + size_t tag_len = std::strlen(tag); + + // Pre-hash the tag (BIP-340 tagged hash convention). + uint8_t tag_hash[32]; + SHA256_CTX ctx; + SHA256_Init(&ctx); + SHA256_Update(&ctx, tag, tag_len); + SHA256_Final(tag_hash, &ctx); + + // SHA256(tag_hash || tag_hash || R.x || P.x || msg) + SHA256_Init(&ctx); + SHA256_Update(&ctx, tag_hash, 32); + SHA256_Update(&ctx, tag_hash, 32); + SHA256_Update(&ctx, r_bytes, 32); + SHA256_Update(&ctx, pk_bytes, 32); + SHA256_Update(&ctx, msg, msg_len); + SHA256_Final(hash, &ctx); + } + + /// Compute sqrt(y2) mod p, choosing the even root. + /// For secp256k1, p = 3 mod 4, so sqrt = y2^((p+1)/4). + Elt sqrt_even(const Elt& y2, const Field& F) const { + // (p+1)/4 for p = 2^256 - 2^32 - 977. + Nat exp( + "0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffff0c"); + Elt root = F.one(); + Elt base = y2; + for (int i = 255; i >= 0; --i) { + root = F.mulf(root, root); + if (exp.bit(i)) { + root = F.mulf(root, base); + } + } + // Pick the even root: convert both candidates to normal form, check LSB. + Nat r0 = F.from_montgomery(root); + if ((r0.bit(0)) == 0) { + return root; + } else { + return F.negf(root); + } + } + + /// Compute intermediate points for scalar multiplication Q = k * P. + void compute_scalar_mult_witness( + Elt bits[kBits], Elt int_x[kBits], Elt int_y[kBits], Elt int_z[kBits], + const typename EC::ECPoint& P, const Nat& k) const { + const Field& F = ec_.f_; + const Elt one = F.one(); + const Elt zero = F.zero(); + + Elt aX = zero, aY = one, aZ = zero; + + for (size_t i = 0; i < kBits; ++i) { + // MSB to LSB (same convention as pk_circuit.h). + size_t bit_idx = kBits - 1 - i; + int bit = k.bit(bit_idx); + bits[i] = F.of_scalar(bit); + + ec_.doubleE(aX, aY, aZ, aX, aY, aZ); + + if (bit == 1) { + ec_.addE(aX, aY, aZ, aX, aY, aZ, P.x, P.y, P.z); + } else { + ec_.addE(aX, aY, aZ, aX, aY, aZ, zero, one, zero); + } + + int_x[i] = aX; + int_y[i] = aY; + int_z[i] = aZ; + } + } + + /// Return the secp256k1 curve order as a Nat. + static Nat scalar_order_nat() { + return Nat( + "0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + } + +}; + +} // namespace proofs + +#endif // PRIVACY_PROOFS_ZK_LIB_CIRCUITS_TESTS_CONTRIB_BIP340_BIP340_WITNESS_H_ diff --git a/lib/circuits/tests/contrib/bip340/specs/code/bip340.py b/lib/circuits/tests/contrib/bip340/specs/code/bip340.py new file mode 100644 index 00000000..b801b5f8 --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/specs/code/bip340.py @@ -0,0 +1,240 @@ +""" +BIP-340 Schnorr signature verification over secp256k1. + +Reference implementation in Sage for testing against the C++ +Bip340Verify circuit. Uses Sage's built-in elliptic curve +arithmetic with hashlib for SHA-256. +""" + +import hashlib +from typing import Any, Optional, Tuple, TypedDict + +import sage.all # type: ignore[import-untyped] + + +# secp256k1 parameters ------------------------------------------------------- +P256K1_P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +P256K1_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +P256K1_B = 7 + + +class Bip340SemanticFacts(TypedDict): + """Affine facts shared with the C++ BIP-340 golden fixtures.""" + + e: int + py: int + ry: int + rz_inv: int + + +# secp256k1 generator (affine). +_GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 +_GY = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 + +# Lazy-initialized Sage objects (avoid module-level Sage for mypy). +_curve = None # type: ignore[var-annotated] +_generator = None # type: ignore[var-annotated] + + +def _get_curve() -> Any: + """Return the secp256k1 elliptic curve (Sage object).""" + global _curve + if _curve is None: + field = getattr(sage.all, "GF")(P256K1_P) + _curve = getattr(sage.all, "EllipticCurve")(field, [0, P256K1_B]) + return _curve + + +def _get_G() -> Any: + """Return the secp256k1 generator point (Sage object).""" + global _generator + if _generator is None: + _generator = _get_curve()(_GX, _GY) + return _generator + + +# BIP-340 tagged hash tag. +_CHALLENGE_TAG = b"BIP0340/challenge" +_TAG_HASH = hashlib.sha256(_CHALLENGE_TAG).digest() + + +def _tagged_hash(data: bytes) -> bytes: + """BIP-340 tagged hash: SHA256(SHA256(tag) || SHA256(tag) || data).""" + return hashlib.sha256(_TAG_HASH + _TAG_HASH + data).digest() + + +def _int_from_bytes(b: bytes) -> int: + """Convert bytes to integer (big-endian).""" + return int.from_bytes(b, "big") + + +def _bytes_from_int(x: int) -> bytes: + """Convert integer to 32-byte big-endian.""" + return x.to_bytes(32, "big") + + +def _is_even(y: int) -> bool: + """Check if an integer is even.""" + return (y & 1) == 0 + + +def lift_x(x: int) -> Optional[Tuple[int, int]]: + """ + BIP-340 lift_x: given an x-coordinate, compute the corresponding + curve point with even y-coordinate. Returns (x, y) or None if + x is not on the curve. + """ + if x >= P256K1_P: + return None + y_sq = (pow(x, 3, P256K1_P) + P256K1_B) % P256K1_P + # p ≡ 3 mod 4, so sqrt is y_sq^((p+1)/4). + y = pow(y_sq, (P256K1_P + 1) // 4, P256K1_P) + if (y * y) % P256K1_P != y_sq: + return None # not a quadratic residue + if not _is_even(y): + y = P256K1_P - y + return (x, y) + + +def verify(pk_bytes: bytes, msg: bytes, sig_bytes: bytes) -> bool: + """ + BIP-340 Schnorr signature verification. + + Args: + pk_bytes: 32-byte x-only public key. + msg: message bytes. + sig_bytes: 64-byte signature (r || s). + + Returns True if the signature is valid. + """ + if len(pk_bytes) != 32 or len(sig_bytes) != 64: + return False + + r_bytes = sig_bytes[:32] + s_bytes = sig_bytes[32:] + + r = _int_from_bytes(r_bytes) + s = _int_from_bytes(s_bytes) + + if r >= P256K1_P or s >= P256K1_N: + return False + + # Lift public key. + pk = lift_x(_int_from_bytes(pk_bytes)) + if pk is None: + return False + P = _get_curve()(pk[0], pk[1]) + + # Challenge e = tagged_hash("BIP0340/challenge", r || P.x || msg) mod n. + e_bytes = _tagged_hash(r_bytes + pk_bytes + msg) + e = _int_from_bytes(e_bytes) % P256K1_N + + # Compute R' = s·G - e·P. + sG = int(s) * _get_G() + eP = int(e) * P + Rp = sG - eP + + if Rp.is_zero(): + return False + + # Normalize and check x-coordinate + even y. + Rx = int(Rp[0]) + Ry = int(Rp[1]) + if not _is_even(Ry): + return False + return Rx == r + + +def semantic_facts( + pk_bytes: bytes, + msg: bytes, + sig_bytes: bytes, +) -> Optional[Bip340SemanticFacts]: + """Compute affine facts that are invariant across Sage and C++ models.""" + if len(pk_bytes) != 32 or len(sig_bytes) != 64: + return None + + r_bytes = sig_bytes[:32] + s = _int_from_bytes(sig_bytes[32:]) + px = _int_from_bytes(pk_bytes) + lifted = lift_x(px) + if lifted is None: + return None + + e = _int_from_bytes(_tagged_hash(r_bytes + pk_bytes + msg)) % P256K1_N + curve = _get_curve() + point = curve(lifted[0], lifted[1]) + result = int(s) * _get_G() - int(e) * point + if result.is_zero(): + return None + + rz = int(result[2]) + rz_inv = int(getattr(sage.all, "GF")(P256K1_P)(rz) ** -1) + return { + 'e': e % P256K1_P, + 'py': lifted[1], + 'ry': int(result[1]), + 'rz_inv': rz_inv, + } + + +class _TestVector(TypedDict): + pk: bytes + msg: bytes + sig: bytes + valid: bool + + +def _load_test_vectors() -> list[_TestVector]: + """Load BIP-340 test vectors from the shared CSV fixture. + + Returns a list of dicts with keys: pk, msg, sig, valid. + """ + import csv + import os + + # Find the CSV relative to this source file. + this_dir = os.path.dirname(os.path.abspath(__file__)) + + csv_path = os.path.join(this_dir, '..', '..', '..', + 'lib', 'circuits', 'bip340', 'testdata', + 'bip340_test_vectors.csv') + if not os.path.exists(csv_path): + raise FileNotFoundError("bip340_test_vectors.csv not found at " + + csv_path) + + vectors: list[_TestVector] = [] + with open(csv_path, newline='', encoding='ascii') as f: + reader = csv.DictReader(f) + for row in reader: + vectors.append({ + 'pk': bytes.fromhex(row['public key']), + 'msg': bytes.fromhex(row['message']) if row['message'] else b'', + 'sig': bytes.fromhex(row['signature']), + 'valid': row['verification result'].upper() == 'TRUE', + }) + assert len(vectors) == 19, f"Expected 19 vectors, got {len(vectors)}" + # Vector 18 has a 100-byte message (200 hex chars). + assert len(vectors[18]['msg']) == 100, ( + f"Vector 18 msg len {len(vectors[18]['msg'])} != 100") + return vectors + + +# BIP-340 test vectors loaded from shared CSV fixture. +# Source: Bitcoin Core bip340_test_vectors.csv +TEST_VECTORS = _load_test_vectors() + + +def curve_order() -> int: + """Return the secp256k1 curve order.""" + return P256K1_N + + +def field_modulus() -> int: + """Return the secp256k1 base field modulus.""" + return P256K1_P + + +def generator() -> Tuple[int, int]: + """Return the secp256k1 generator point (affine).""" + return (_GX, _GY) diff --git a/lib/circuits/tests/contrib/bip340/specs/code/generate_bip340_vectors_inc.py b/lib/circuits/tests/contrib/bip340/specs/code/generate_bip340_vectors_inc.py new file mode 100644 index 00000000..a251979b --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/specs/code/generate_bip340_vectors_inc.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Regenerate BIP-340 C++ fixtures from checked-in testdata. + +By default this script reads the Bitcoin Core CSV already stored at +lib/circuits/tests/contrib/bip340/testdata/bip340_test_vectors.csv and regenerates: + + - lib/circuits/tests/contrib/bip340/testdata/bip340_vectors.inc + - lib/circuits/tests/contrib/bip340/testdata/bip340_golden.inc + +Use --refresh-bitcoin-core to replace the checked-in CSV from upstream first. +""" + +import argparse +import csv +import json +from pathlib import Path +from urllib.request import urlopen + + +BITCOIN_CORE_BIP340_CSV = ( + "https://raw.githubusercontent.com/bitcoin/bitcoin/refs/heads/master/" + "test/functional/test_framework/bip340_test_vectors.csv" +) + +SCRIPT_DIR = Path(__file__).resolve().parent +TESTDATA_DIR = SCRIPT_DIR.parents[1] / "testdata" +CSV_PATH = TESTDATA_DIR / "bip340_test_vectors.csv" +JSON_PATH = TESTDATA_DIR / "bip340_golden.json" +VECTORS_INC_PATH = TESTDATA_DIR / "bip340_vectors.inc" +GOLDEN_INC_PATH = TESTDATA_DIR / "bip340_golden.inc" + + +# Exact Bitcoin Core CSV headers. Regeneration fails if upstream changes. +EXPECTED_HEADERS = [ + "index", "secret key", "public key", "aux_rand", "message", + "signature", "verification result", "comment", +] + + +def cell(row: dict[str, str], name: str) -> str: + """Read a CSV field by Bitcoin Core column name.""" + if name in row: + return row[name] + raise KeyError(f"missing CSV column {name!r}; got {sorted(row)}") + + +def cxx_string(value: str) -> str: + """Return a C++ string literal split into readable 64-char chunks.""" + if not value: + return '""' + chunks = [value[i:i + 64] for i in range(0, len(value), 64)] + return "\n ".join(f'"{chunk}"' for chunk in chunks) + + +def load_rows() -> list[dict[str, str]]: + with CSV_PATH.open(newline="", encoding="ascii") as f: + reader = csv.DictReader(f) + actual = reader.fieldnames + if actual is None or list(actual) != EXPECTED_HEADERS: + raise ValueError( + f"CSV headers changed.\n" + f" expected: {EXPECTED_HEADERS}\n" + f" got: {actual}" + ) + rows = list(reader) + if len(rows) != 19: + raise ValueError(f"expected 19 BIP-340 vectors, got {len(rows)}") + return rows + + +def refresh_csv() -> None: + with urlopen(BITCOIN_CORE_BIP340_CSV, timeout=30) as response: + data = response.read() + CSV_PATH.write_bytes(data) + + +def write_vectors_inc(rows: list[dict[str, str]]) -> None: + with VECTORS_INC_PATH.open("w", encoding="ascii", newline="\n") as f: + f.write("// Auto-generated from bip340_test_vectors.csv. Do not edit.\n") + f.write("// Source: Bitcoin Core BIP-340 test vectors.\n") + f.write(f"// Regenerate: {Path(__file__).name}\n\n") + for i, row in enumerate(rows): + valid = ( + "true" + if cell(row, "verification result").upper() == "TRUE" + else "false" + ) + comment = cell(row, "comment") + if comment: + f.write(f" // {i}: {comment}\n") + else: + f.write(f" // {i}\n") + f.write(" {") + f.write(f'{cxx_string(cell(row, "public key"))},\n') + f.write(f' {cxx_string(cell(row, "message"))},\n') + f.write(f' {cxx_string(cell(row, "signature"))}, {valid}}},\n') + + +def write_golden_inc() -> None: + facts = json.loads(JSON_PATH.read_text(encoding="ascii")) + if len(facts) != 19: + raise ValueError(f"expected 19 golden facts, got {len(facts)}") + + with GOLDEN_INC_PATH.open("w", encoding="ascii", newline="\n") as f: + f.write("// Auto-generated from bip340_golden.json. Do not edit.\n") + f.write(f"// Regenerate: {Path(__file__).name}\n\n") + for fact in facts: + compute_success = "false" if "compute_error" in fact else "true" + valid = "true" if fact["valid"] else "false" + f.write(f" // {fact['index']}\n") + f.write( + f' {{{fact["index"]}, {valid}, {compute_success},\n' + f' "{fact.get("rx_hex", "")}",\n' + f' "{fact.get("px_hex", "")}",\n' + f' "{fact.get("e_hex", "")}",\n' + f' "{fact.get("py_hex", "")}",\n' + f' "{fact.get("ry_hex", "")}"}},\n' + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--refresh-bitcoin-core", + action="store_true", + help="download the upstream Bitcoin Core BIP-340 CSV before regenerating", + ) + args = parser.parse_args() + + if args.refresh_bitcoin_core: + refresh_csv() + + rows = load_rows() + write_vectors_inc(rows) + write_golden_inc() + print(f"Regenerated {VECTORS_INC_PATH}") + print(f"Regenerated {GOLDEN_INC_PATH}") + + +if __name__ == "__main__": + main() diff --git a/lib/circuits/tests/contrib/bip340/specs/code/run_bip340_sage_tests.sh b/lib/circuits/tests/contrib/bip340/specs/code/run_bip340_sage_tests.sh new file mode 100755 index 00000000..94bf8032 --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/specs/code/run_bip340_sage_tests.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Run all Sage BIP-340 tests. Requires Sage (importable via sage.all). +# Usage: ./run_bip340_sage_tests.sh + +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../../../../../" && pwd)" +SHARED_SAGE_DIR="$REPO_ROOT/docs/specs/sage" + +if [[ ! -d "$SHARED_SAGE_DIR" ]]; then + printf 'ERROR: Shared Sage specs not found at %s\n' "$SHARED_SAGE_DIR" >&2 + exit 1 +fi + +cd "$SCRIPT_DIR" + +export PYTHONPATH="$SCRIPT_DIR:$SHARED_SAGE_DIR${PYTHONPATH:+:$PYTHONPATH}" + +# Find a Python that can import sage.all. +SAGE_PYTHON_CMD=() +SAGE_PYTHON_LABEL="" + +try_sage_python() { + if "$@" -c "import sage.all" 2>/dev/null; then + SAGE_PYTHON_CMD=("$@") + SAGE_PYTHON_LABEL="$*" + return 0 + fi + return 1 +} + +if command -v python3 &>/dev/null; then + try_sage_python python3 || true +fi + +if [ "${#SAGE_PYTHON_CMD[@]}" -eq 0 ] && command -v sage &>/dev/null; then + try_sage_python sage -python || true +fi + +if [ "${#SAGE_PYTHON_CMD[@]}" -eq 0 ]; then + printf '%s\n' "ERROR: No Python with 'sage.all' found." >&2 + printf '%s\n' "Install SageMath or make its Python interpreter available." >&2 + exit 1 +fi + +printf 'Using Sage Python: %s\n' "$SAGE_PYTHON_LABEL" + +printf '%s\n' "=== Sage BIP-340 tests ===" +"${SAGE_PYTHON_CMD[@]}" -m unittest discover -s tests -p 'test_bip340*.py' -v + +printf '\n%s\n' "=== All Sage BIP-340 tests complete ===" diff --git a/lib/circuits/tests/contrib/bip340/specs/code/tests/test_bip340.py b/lib/circuits/tests/contrib/bip340/specs/code/tests/test_bip340.py new file mode 100644 index 00000000..e8112f28 --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/specs/code/tests/test_bip340.py @@ -0,0 +1,125 @@ +"""Tests for BIP-340 Schnorr verification (Sage reference implementation).""" + +import unittest + +from bip340 import ( + TEST_VECTORS, + curve_order, + field_modulus, + generator, + lift_x, + verify, +) + + +class TestBip340(unittest.TestCase): + def test_curve_params(self) -> None: + """Secp256k1 parameters are well-formed.""" + p = field_modulus() + n = curve_order() + Gx, Gy = generator() + + # p is odd prime, n is prime. + self.assertEqual(p % 2, 1) + self.assertGreater(n, 1) + self.assertLess(n, p) + + # Generator is on the curve: y² = x³ + 7 mod p. + lhs = (Gy * Gy) % p + rhs = (pow(Gx, 3, p) + 7) % p + self.assertEqual(lhs, rhs) + + def test_lift_x(self) -> None: + """lift_x computes the correct even-y point.""" + # G.x lifted should give G. + Gx, Gy = generator() + pt = lift_x(Gx) + self.assertIsNotNone(pt) + assert pt is not None # type narrowing for mypy + self.assertEqual(pt[0], Gx) + self.assertEqual(pt[1], Gy) + # Gy should be even. + self.assertEqual(Gy & 1, 0) + + # x not on curve returns None. + self.assertIsNone(lift_x(0)) + + def test_lift_x_even_y(self) -> None: + """lift_x always returns an even y-coordinate.""" + from bip340 import _get_curve, _get_G + E = _get_curve() + G = _get_G() + for i in range(5): + x = int(((i + 2) * G)[0]) + pt = lift_x(x) + if pt is not None: + self.assertEqual(pt[1] & 1, 0, f"lift_x({hex(x)}) gave odd y") + + def test_test_vectors(self) -> None: + """Verify all known BIP-340 test vectors.""" + for i, tv in enumerate(TEST_VECTORS): + with self.subTest(vector=i): + result = verify(tv["pk"], tv["msg"], tv["sig"]) + self.assertEqual( + result, tv["valid"], + f"Vector {i}: expected {tv['valid']}, got {result}" + ) + + def test_empty_signature_fails(self) -> None: + """Empty/zero signature is rejected.""" + tv = TEST_VECTORS[0] + self.assertFalse(verify(tv["pk"], tv["msg"], bytes(64))) + + def test_zero_public_key_fails(self) -> None: + """x=0 is not a valid public key (no even-y point).""" + tv = TEST_VECTORS[0] + self.assertFalse(verify(bytes(32), tv["msg"], tv["sig"])) + + def test_tampered_signature_fails(self) -> None: + """Flipping a bit in a valid signature invalidates it.""" + tv = TEST_VECTORS[0] + sig = bytearray(tv["sig"]) + sig[10] ^= 0x01 + self.assertFalse(verify(tv["pk"], tv["msg"], bytes(sig))) + + def test_wrong_lengths_rejected(self) -> None: + """Malformed inputs are rejected gracefully.""" + tv = TEST_VECTORS[0] + self.assertFalse(verify(b"x" * 31, tv["msg"], tv["sig"])) + self.assertFalse(verify(b"x" * 33, tv["msg"], tv["sig"])) + self.assertFalse(verify(tv["pk"], tv["msg"], b"x" * 63)) + self.assertFalse(verify(tv["pk"], tv["msg"], b"x" * 65)) + + def test_scalars_s_g(self) -> None: + """Verify that s·G and e·P are computed correctly.""" + # Use test vector 0. + tv = TEST_VECTORS[0] + pk_bytes = tv["pk"] + sig = tv["sig"] + + from bip340 import ( + _get_curve, _get_G, _int_from_bytes, _tagged_hash, P256K1_N, lift_x, + ) + + r_bytes = sig[:32] + s_bytes = sig[32:] + r = _int_from_bytes(r_bytes) + s = _int_from_bytes(s_bytes) + e = _int_from_bytes( + _tagged_hash(r_bytes + pk_bytes + tv["msg"]) + ) % P256K1_N + + E = _get_curve() + G = _get_G() + pk_pt = lift_x(_int_from_bytes(pk_bytes)) + assert pk_pt is not None + P = E(pk_pt[0], pk_pt[1]) + + # s·G - e·P should equal R. + R = int(s) * G - int(e) * P + self.assertFalse(R.is_zero()) + self.assertEqual(int(R[0]), r) + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/circuits/tests/contrib/bip340/specs/code/tests/test_bip340_golden.py b/lib/circuits/tests/contrib/bip340/specs/code/tests/test_bip340_golden.py new file mode 100644 index 00000000..28d431d8 --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/specs/code/tests/test_bip340_golden.py @@ -0,0 +1,79 @@ +"""Sage golden-fact tests for BIP-340. + +Compares Sage-generated field values against a checked-in golden.json +fixture. The C++ tests separately compare the semantic witness facts +that are invariant across the Sage affine model and C++ projective model. +""" + +import json +import unittest +from pathlib import Path + +from bip340 import semantic_facts + +TESTDATA_DIR = Path(__file__).resolve().parents[3] / "testdata" +GOLDEN_PATH = TESTDATA_DIR / "bip340_golden.json" + +with GOLDEN_PATH.open(encoding="ascii") as f: + GOLDEN = json.load(f) + + +class TestBip340Golden(unittest.TestCase): + + def test_count(self) -> None: + """Golden facts cover all 19 vectors.""" + self.assertEqual(len(GOLDEN), 19) + + def test_vector_18_length(self) -> None: + """Vector 18 has 100-byte message.""" + # Check via the CSV, not the golden file. + import csv + csv_path = TESTDATA_DIR / "bip340_test_vectors.csv" + with csv_path.open(encoding="ascii") as f: + rows = list(csv.DictReader(f)) + self.assertEqual(len(rows[18]['message']), 200, + "Vector 18 message hex should be 200 chars (100 bytes)") + + def test_golden_facts_match_sage(self) -> None: + """For every valid vector, Sage recomputation matches golden.""" + import csv + csv_path = TESTDATA_DIR / "bip340_test_vectors.csv" + with csv_path.open(encoding="ascii") as f: + rows = list(csv.DictReader(f)) + + for i, row in enumerate(rows): + pk = bytes.fromhex(row['public key']) + msg = bytes.fromhex(row['message']) if row['message'] else b'' + sig = bytes.fromhex(row['signature']) + valid = row['verification result'].upper() == 'TRUE' + + fact = GOLDEN[i] + self.assertEqual(fact['index'], i) + self.assertEqual(fact['valid'], valid) + + if 'compute_error' in fact: + # This vector cannot be witnessed — must be invalid. + self.assertFalse(valid, + f"Vector {i}: compute error but marked valid") + continue + + # Recompute and compare. + facts = semantic_facts(pk, msg, sig) + self.assertIsNotNone(facts, f"Vector {i}: no semantic facts") + assert facts is not None + + def fe_hex(value: int) -> str: + return hex(value)[2:].upper().zfill(64) + + self.assertEqual(fe_hex(facts['e']), fact['e_hex'], + f"Vector {i}: e mismatch") + self.assertEqual(fe_hex(facts['py']), fact['py_hex'], + f"Vector {i}: py mismatch") + self.assertEqual(fe_hex(facts['ry']), fact['ry_hex'], + f"Vector {i}: ry mismatch") + self.assertEqual(fe_hex(facts['rz_inv']), fact['rz_inv_hex'], + f"Vector {i}: rz_inv mismatch") + + +if __name__ == '__main__': + unittest.main() diff --git a/lib/circuits/tests/contrib/bip340/testdata/bip340_golden.inc b/lib/circuits/tests/contrib/bip340/testdata/bip340_golden.inc new file mode 100644 index 00000000..c8b6d041 --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/testdata/bip340_golden.inc @@ -0,0 +1,136 @@ +// Auto-generated from bip340_golden.json. Do not edit. +// Regenerate: generate_bip340_vectors_inc.py + + // 0 + {0, true, true, + "E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA8215", + "F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "6BB6B93A91F2ECC0CD924F4F9BAABB5E6EB21745BB00F2CEBDAAC908BB5D86CE", + "388F7B0F632DE8140FE337E62A37F3566500A99934C2231B6CB9FD7584B8E672", + "7B64F7B795A4E915A02FF65C521B8D4B75D237E851143CC54B05DA1342A65D08"}, + // 1 + {1, true, true, + "6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE3341", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "CFB58E748D9648B71FDC909FB7432FC0C954DA5BD75CDC9D4804D32648F9839A", + "2CE19B946C4EE58546F5251D441A065EA50735606985E5B228788BEC4E582898", + "20BC7663DA14BE43C22EB2CCB49CD746573B2766B277273FCB20A2F6C1A60C6C"}, + // 2 + {2, true, true, + "5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1B", + "DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", + "9BC1BA4A0ABBC0792066B2CA0EF771D88AF676B322A83DD7517F7C1FD149215A", + "F594BB5F72B37FAAE396A4259EA64ED5E6FDEB2A51C6467582B275925FAB1394", + "5F193D22F6F1D925A7F8C4CEFF20CC2A53BA1C3310CA843CF83156C1514BB284"}, + // 3 + {3, true, true, + "7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC", + "25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517", + "A5352F4DED269E6716184547944A209417F759DF44E61EA1D48008997C9003C0", + "F3014853BCBE349BBE38FDA97573F042378C4F30A37E7E02501C51180B632786", + "434B611A10B7CAFCDD0F94CECE5646A9781B181F42E8C41BCA5E16A7BB8CE194"}, + // 4 + {4, true, true, + "00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C63", + "D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9", + "B6408F581E891B7E0B64B847F699B6A38A0CE38532AC2D6E113804BD36262375", + "7B5DC8FF3A3D451427A36C4A602A5F8AF7087BCC07EAF717DB2D2CBCA1A1B926", + "C0C686408D517DFD67C2367651380D00D126E4229631FD03F8FF35EEF1A61E3C"}, + // 5 + {5, false, false, + "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769", + "EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34", + "", + "", + ""}, + // 6 + {6, false, true, + "FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A1460297556", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "6A3C2BBDE36B8F3E7AA3393D0648A63360B644E2A6C67E465EE64099CC3A5BFB", + "2CE19B946C4EE58546F5251D441A065EA50735606985E5B228788BEC4E582898", + "AE12777AACFBB620F3BE96017F45C560DE80F0F6518FE4A03C870C36B075F297"}, + // 7 + {7, false, true, + "1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "030EEB3787BB88BC841ED90014A523F78A8E9F271D653716D1B7DE2FC35A2C03", + "2CE19B946C4EE58546F5251D441A065EA50735606985E5B228788BEC4E582898", + "9130FE7C2C5A20C6B0E6B8EE0E9DA2026B169EF3DAAA35C76EBC848269901E13"}, + // 8 + {8, false, true, + "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "E1929C648D59703679196FFB9E1D5BA15EFC3DEF340A8BEC1EE69239DF801B8F", + "2CE19B946C4EE58546F5251D441A065EA50735606985E5B228788BEC4E582898", + "078D353E7470A75BA43F6601A947CD252E8DEFF8812DC986A6FD7DCDE0D375C2"}, + // 9 + {9, false, false, + "0000000000000000000000000000000000000000000000000000000000000000", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "", + "", + ""}, + // 10 + {10, false, false, + "0000000000000000000000000000000000000000000000000000000000000001", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "", + "", + ""}, + // 11 + {11, false, true, + "4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "21ABA58EFC3D4132C2C0C2BDB5B24CB074D73EE4DFBBF033B497C2F710E8E83E", + "2CE19B946C4EE58546F5251D441A065EA50735606985E5B228788BEC4E582898", + "AC6734BCEF06DA0437BF3E52345F94F3D7D6FD2FEB73AEAA6997211F5CAC9F36"}, + // 12 + {12, false, true, + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "B8E57FFA266E46C75745E3CB582FFDD216EFA355CCF0F499AF3FDD2C429D7590", + "2CE19B946C4EE58546F5251D441A065EA50735606985E5B228788BEC4E582898", + "9C5958D7EB74C7FFE38AD47BC8FAC9369B517C4B7794431DA201488F35DFB492"}, + // 13 + {13, false, true, + "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "E1929C648D59703679196FFB9E1D5BA15EFC3DEF340A8BEC1EE69239DF801B8F", + "2CE19B946C4EE58546F5251D441A065EA50735606985E5B228788BEC4E582898", + "E282882E0FBF2CC0ABC173B55F93B19D55E41D9CD47596DCB86D857B7D766362"}, + // 14 + {14, false, false, + "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + "", + "", + ""}, + // 15 + {15, true, true, + "71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF", + "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "D3561670CD8B50B077CD4E229A6E1C09A1A663AD08264632A8596194B9E0ABC7", + "C0ABC6F36C165BC8AA22CB487917864C7FEE767DDE43C7618B9B12A516A674D2", + "5D5066512B6F78F310B9452FC6719F6645B23DF3AF9287C3FDBD6A2649E0BECC"}, + // 16 + {16, true, true, + "08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303", + "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "4535CF052478F0F45F226501CF68DD6898143DE5D6905E7F6607A1C6B4323A60", + "C0ABC6F36C165BC8AA22CB487917864C7FEE767DDE43C7618B9B12A516A674D2", + "814C7E699159D5EBD1C9B941F2F4B6014E5E71227DCA2B645EF40CE02DA9B0F0"}, + // 17 + {17, true, true, + "5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370", + "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "63F0AAC9B6B870317DEE839C5F5A7F1B584D0CD82C2BBA4002CFCA02DDEF9B74", + "C0ABC6F36C165BC8AA22CB487917864C7FEE767DDE43C7618B9B12A516A674D2", + "58FCF1954A00E5DF4A285940B5823D3CB4A3FC9344CE8FF4D1C49A9CD17445B6"}, + // 18 + {18, true, true, + "403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8", + "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "44DD9FE38BA9FEFA45D2ACA76A95FF66CC535E11FD695B65448374F2016EFAE3", + "C0ABC6F36C165BC8AA22CB487917864C7FEE767DDE43C7618B9B12A516A674D2", + "3C3B7748FFEB13A328493A67301586744F69682B1927BF8A60BD175EDBDF6066"}, diff --git a/lib/circuits/tests/contrib/bip340/testdata/bip340_golden.json b/lib/circuits/tests/contrib/bip340/testdata/bip340_golden.json new file mode 100644 index 00000000..d6ae5868 --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/testdata/bip340_golden.json @@ -0,0 +1,180 @@ +[ + { + "index": 0, + "valid": true, + "rx_hex": "E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA8215", + "px_hex": "F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "e_hex": "6BB6B93A91F2ECC0CD924F4F9BAABB5E6EB21745BB00F2CEBDAAC908BB5D86CE", + "py_hex": "388F7B0F632DE8140FE337E62A37F3566500A99934C2231B6CB9FD7584B8E672", + "ry_hex": "7B64F7B795A4E915A02FF65C521B8D4B75D237E851143CC54B05DA1342A65D08", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "index": 1, + "valid": true, + "rx_hex": "6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE3341", + "px_hex": "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "e_hex": "CFB58E748D9648B71FDC909FB7432FC0C954DA5BD75CDC9D4804D32648F9839A", + "py_hex": "2CE19B946C4EE58546F5251D441A065EA50735606985E5B228788BEC4E582898", + "ry_hex": "20BC7663DA14BE43C22EB2CCB49CD746573B2766B277273FCB20A2F6C1A60C6C", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "index": 2, + "valid": true, + "rx_hex": "5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1B", + "px_hex": "DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", + "e_hex": "9BC1BA4A0ABBC0792066B2CA0EF771D88AF676B322A83DD7517F7C1FD149215A", + "py_hex": "F594BB5F72B37FAAE396A4259EA64ED5E6FDEB2A51C6467582B275925FAB1394", + "ry_hex": "5F193D22F6F1D925A7F8C4CEFF20CC2A53BA1C3310CA843CF83156C1514BB284", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "index": 3, + "valid": true, + "rx_hex": "7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC", + "px_hex": "25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517", + "e_hex": "A5352F4DED269E6716184547944A209417F759DF44E61EA1D48008997C9003C0", + "py_hex": "F3014853BCBE349BBE38FDA97573F042378C4F30A37E7E02501C51180B632786", + "ry_hex": "434B611A10B7CAFCDD0F94CECE5646A9781B181F42E8C41BCA5E16A7BB8CE194", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "index": 4, + "valid": true, + "rx_hex": "00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C63", + "px_hex": "D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9", + "e_hex": "B6408F581E891B7E0B64B847F699B6A38A0CE38532AC2D6E113804BD36262375", + "py_hex": "7B5DC8FF3A3D451427A36C4A602A5F8AF7087BCC07EAF717DB2D2CBCA1A1B926", + "ry_hex": "C0C686408D517DFD67C2367651380D00D126E4229631FD03F8FF35EEF1A61E3C", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "index": 5, + "valid": false, + "rx_hex": "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769", + "px_hex": "EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34", + "compute_error": "" + }, + { + "index": 6, + "valid": false, + "rx_hex": "FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A1460297556", + "px_hex": "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "e_hex": "6A3C2BBDE36B8F3E7AA3393D0648A63360B644E2A6C67E465EE64099CC3A5BFB", + "py_hex": "2CE19B946C4EE58546F5251D441A065EA50735606985E5B228788BEC4E582898", + "ry_hex": "AE12777AACFBB620F3BE96017F45C560DE80F0F6518FE4A03C870C36B075F297", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "index": 7, + "valid": false, + "rx_hex": "1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F", + "px_hex": "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "e_hex": "030EEB3787BB88BC841ED90014A523F78A8E9F271D653716D1B7DE2FC35A2C03", + "py_hex": "2CE19B946C4EE58546F5251D441A065EA50735606985E5B228788BEC4E582898", + "ry_hex": "9130FE7C2C5A20C6B0E6B8EE0E9DA2026B169EF3DAAA35C76EBC848269901E13", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "index": 8, + "valid": false, + "rx_hex": "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769", + "px_hex": "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "e_hex": "E1929C648D59703679196FFB9E1D5BA15EFC3DEF340A8BEC1EE69239DF801B8F", + "py_hex": "2CE19B946C4EE58546F5251D441A065EA50735606985E5B228788BEC4E582898", + "ry_hex": "078D353E7470A75BA43F6601A947CD252E8DEFF8812DC986A6FD7DCDE0D375C2", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "index": 9, + "valid": false, + "rx_hex": "0000000000000000000000000000000000000000000000000000000000000000", + "px_hex": "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "compute_error": "" + }, + { + "index": 10, + "valid": false, + "rx_hex": "0000000000000000000000000000000000000000000000000000000000000001", + "px_hex": "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "compute_error": "" + }, + { + "index": 11, + "valid": false, + "rx_hex": "4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D", + "px_hex": "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "e_hex": "21ABA58EFC3D4132C2C0C2BDB5B24CB074D73EE4DFBBF033B497C2F710E8E83E", + "py_hex": "2CE19B946C4EE58546F5251D441A065EA50735606985E5B228788BEC4E582898", + "ry_hex": "AC6734BCEF06DA0437BF3E52345F94F3D7D6FD2FEB73AEAA6997211F5CAC9F36", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "index": 12, + "valid": false, + "rx_hex": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", + "px_hex": "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "e_hex": "B8E57FFA266E46C75745E3CB582FFDD216EFA355CCF0F499AF3FDD2C429D7590", + "py_hex": "2CE19B946C4EE58546F5251D441A065EA50735606985E5B228788BEC4E582898", + "ry_hex": "9C5958D7EB74C7FFE38AD47BC8FAC9369B517C4B7794431DA201488F35DFB492", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "index": 13, + "valid": false, + "rx_hex": "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769", + "px_hex": "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "e_hex": "E1929C648D59703679196FFB9E1D5BA15EFC3DEF340A8BEC1EE69239DF801B8F", + "py_hex": "2CE19B946C4EE58546F5251D441A065EA50735606985E5B228788BEC4E582898", + "ry_hex": "E282882E0FBF2CC0ABC173B55F93B19D55E41D9CD47596DCB86D857B7D766362", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "index": 14, + "valid": false, + "rx_hex": "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769", + "px_hex": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + "compute_error": "" + }, + { + "index": 15, + "valid": true, + "rx_hex": "71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF", + "px_hex": "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "e_hex": "D3561670CD8B50B077CD4E229A6E1C09A1A663AD08264632A8596194B9E0ABC7", + "py_hex": "C0ABC6F36C165BC8AA22CB487917864C7FEE767DDE43C7618B9B12A516A674D2", + "ry_hex": "5D5066512B6F78F310B9452FC6719F6645B23DF3AF9287C3FDBD6A2649E0BECC", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "index": 16, + "valid": true, + "rx_hex": "08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303", + "px_hex": "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "e_hex": "4535CF052478F0F45F226501CF68DD6898143DE5D6905E7F6607A1C6B4323A60", + "py_hex": "C0ABC6F36C165BC8AA22CB487917864C7FEE767DDE43C7618B9B12A516A674D2", + "ry_hex": "814C7E699159D5EBD1C9B941F2F4B6014E5E71227DCA2B645EF40CE02DA9B0F0", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "index": 17, + "valid": true, + "rx_hex": "5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370", + "px_hex": "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "e_hex": "63F0AAC9B6B870317DEE839C5F5A7F1B584D0CD82C2BBA4002CFCA02DDEF9B74", + "py_hex": "C0ABC6F36C165BC8AA22CB487917864C7FEE767DDE43C7618B9B12A516A674D2", + "ry_hex": "58FCF1954A00E5DF4A285940B5823D3CB4A3FC9344CE8FF4D1C49A9CD17445B6", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "index": 18, + "valid": true, + "rx_hex": "403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8", + "px_hex": "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "e_hex": "44DD9FE38BA9FEFA45D2ACA76A95FF66CC535E11FD695B65448374F2016EFAE3", + "py_hex": "C0ABC6F36C165BC8AA22CB487917864C7FEE767DDE43C7618B9B12A516A674D2", + "ry_hex": "3C3B7748FFEB13A328493A67301586744F69682B1927BF8A60BD175EDBDF6066", + "rz_inv_hex": "0000000000000000000000000000000000000000000000000000000000000001" + } +] \ No newline at end of file diff --git a/lib/circuits/tests/contrib/bip340/testdata/bip340_test_vectors.csv b/lib/circuits/tests/contrib/bip340/testdata/bip340_test_vectors.csv new file mode 100644 index 00000000..aa317a3b --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/testdata/bip340_test_vectors.csv @@ -0,0 +1,20 @@ +index,secret key,public key,aux_rand,message,signature,verification result,comment +0,0000000000000000000000000000000000000000000000000000000000000003,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0,TRUE, +1,B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,0000000000000000000000000000000000000000000000000000000000000001,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A,TRUE, +2,C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9,DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8,C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906,7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C,5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7,TRUE, +3,0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710,25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3,TRUE,test fails if msg is reduced modulo p or n +4,,D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9,,4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703,00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4,TRUE, +5,,EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key not on the curve +6,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2,FALSE,has_even_y(R) is false +7,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD,FALSE,negated message +8,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6,FALSE,negated s value +9,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0 +10,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1 +11,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is not an X coordinate on the curve +12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size +13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order +14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size +15,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,,71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63,TRUE,message of size 0 (added 2022-12) +16,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,11,08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF,TRUE,message of size 1 (added 2022-12) +17,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,0102030405060708090A0B0C0D0E0F1011,5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5,TRUE,message of size 17 (added 2022-12) +18,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999,403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367,TRUE,message of size 100 (added 2022-12) diff --git a/lib/circuits/tests/contrib/bip340/testdata/bip340_vectors.inc b/lib/circuits/tests/contrib/bip340/testdata/bip340_vectors.inc new file mode 100644 index 00000000..c2a3a412 --- /dev/null +++ b/lib/circuits/tests/contrib/bip340/testdata/bip340_vectors.inc @@ -0,0 +1,102 @@ +// Auto-generated from bip340_test_vectors.csv. Do not edit. +// Source: Bitcoin Core BIP-340 test vectors. +// Regenerate: generate_bip340_vectors_inc.py + + // 0 + {"F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "0000000000000000000000000000000000000000000000000000000000000000", + "E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA8215" + "25F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0", true}, + // 1 + {"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE3341" + "8906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A", true}, + // 2 + {"DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", + "7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C", + "5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1B" + "AB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7", true}, + // 3: test fails if msg is reduced modulo p or n + {"25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC" + "97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3", true}, + // 4 + {"D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9", + "4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703", + "00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C63" + "76AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4", true}, + // 5: public key not on the curve + {"EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769" + "69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", false}, + // 6: has_even_y(R) is false + {"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A1460297556" + "3CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2", false}, + // 7: negated message + {"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F" + "28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD", false}, + // 8: negated s value + {"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769" + "961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6", false}, + // 9: sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0 + {"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "0000000000000000000000000000000000000000000000000000000000000000" + "123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051", false}, + // 10: sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1 + {"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "0000000000000000000000000000000000000000000000000000000000000001" + "7615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197", false}, + // 11: sig[0:32] is not an X coordinate on the curve + {"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D" + "69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", false}, + // 12: sig[0:32] is equal to field size + {"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F" + "69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", false}, + // 13: sig[32:64] is equal to curve order + {"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769" + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", false}, + // 14: public key is not a valid X coordinate because it exceeds the field size + {"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769" + "69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", false}, + // 15: message of size 0 (added 2022-12) + {"778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "", + "71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF" + "6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63", true}, + // 16: message of size 1 (added 2022-12) + {"778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "11", + "08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303" + "EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF", true}, + // 17: message of size 17 (added 2022-12) + {"778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "0102030405060708090A0B0C0D0E0F1011", + "5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370" + "C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5", true}, + // 18: message of size 100 (added 2022-12) + {"778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "9999999999999999999999999999999999999999999999999999999999999999" + "9999999999999999999999999999999999999999999999999999999999999999" + "9999999999999999999999999999999999999999999999999999999999999999" + "99999999", + "403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8" + "585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367", true},