From 693e4878243ad6f242e58e4c7a0932d07b7a3585 Mon Sep 17 00:00:00 2001 From: federicobarbacovi <171914500+federicobarbacovi@users.noreply.github.com> Date: Mon, 18 May 2026 12:31:51 +0100 Subject: [PATCH 1/2] chore: Add decider to benchmark components (#23170) The HypernovaDecider is not showing up in our benchmark breakdown. This PR adds it do the components. It also adds the BatchMerge prover/verifier --- barretenberg/cpp/scripts/extract_component_benchmarks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/barretenberg/cpp/scripts/extract_component_benchmarks.py b/barretenberg/cpp/scripts/extract_component_benchmarks.py index 8565380d5437..1838670385ed 100644 --- a/barretenberg/cpp/scripts/extract_component_benchmarks.py +++ b/barretenberg/cpp/scripts/extract_component_benchmarks.py @@ -31,7 +31,7 @@ benchmarks = [] # Key components to track (case-insensitive matching) - key_components = ["sumcheck", "pcs", "pippenger", "commitment", "circuit", "oink", "compute"] + key_components = ["sumcheck", "pcs", "pippenger", "commitment", "circuit", "oink", "compute", "decider", "BatchMergeProver", "BatchMergeVerifier"] for op_name, entries in data.items(): # Check if this is a key component we want to track From 7e747b6230e03dcc33a913a2ab435beae97b36f6 Mon Sep 17 00:00:00 2001 From: ledwards2225 <98505400+ledwards2225@users.noreply.github.com> Date: Mon, 18 May 2026 09:43:57 -0700 Subject: [PATCH 2/2] feat!: schnorr w/ poseidon2 (#21808) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Switches Schnorr from `pedersen(R.x ‖ pubkey.x ‖ pubkey.y) → blake2s(… ‖ message)` to a single `Poseidon2(R.x, pubkey.x, pubkey.y, message)` end-to-end. Affects the native signer in bbapi, the in-circuit verifier (consumed via noir-lang/schnorr `v0.2.0` → `v0.3.0`), and the on-wire auth witness shape for both schnorr account contracts. ## Changes **Native (cpp)** - `barretenberg/cpp/src/barretenberg/crypto/schnorr/*` — challenge derivation switched to Poseidon2 over `(R.x, pubkey.x, pubkey.y, message_field)`. Drops the `Hash` and `Fq` template parameters; signer/verifier now take the message as a grumpkin base field element directly. - `barretenberg/cpp/src/barretenberg/bbapi/bbapi_schnorr.cpp` — asserts the bbapi message buffer is exactly 32 bytes (a serialized field element) and deserializes it into `grumpkin::fq`. - New pinned test vectors (`pinned_test_vector_small`, `pinned_test_vector_large`) that fix the `(private_key, nonce_k, message) → (public_key, R, s, e)` mapping for cross-implementation regression. **Noir contracts** - `schnorr_account_contract` and `schnorr_hardcoded_account_contract`: bump pinned `noir-lang/schnorr` from `v0.2.0` to `v0.3.0`. - Both `is_valid_impl` callsites rewritten to pass `outer_hash` as `Field` (not `[u8; 32]`) and the signature as `(EmbeddedCurveScalar, EmbeddedCurveScalar)` (not `[u8; 64]`). **TS (bb.js authwit emitters)** - New `SchnorrSignature.toLimbFields()` returning `[s.lo, s.hi, e.lo, e.hi]` (top-16-bytes = hi, bottom-16-bytes = lo, BE). - All four schnorr authwit producers updated: `@aztec/accounts/schnorr`, `txe_oracle_top_level_context`, `end-to-end/fixtures/schnorr_hardcoded_account_contract`, `end-to-end/guides/writing_an_account_contract`. - Foundation schnorr test now signs a 32-byte field-element message (the freeform-string preimage was incompatible with the new bbapi 32-byte assert). **VKs** - Standalone Chonk VKs shift for every flow that compiles a schnorr account contract. - Regenerated `example-app-ivc-inputs-out/` via `build_bench`, re-uploaded `bb-chonk-inputs-45ff0930.tar.gz` to S3, and bumped the pinned hash in `test_chonk_standalone_vks_havent_changed.sh`. All 11 flows pass `prove+verify` against the new inputs. ## In-circuit efficiency Per-tx `is_valid_impl` for schnorr-account flows replaces, in-circuit, pedersen-of-3-inputs + blake2s-of-1-block + 32 byte-range checks for the message + 64 byte-range checks for the authwit, with one Poseidon2 permutation. Net: a few thousand gates removed from every fold step in schnorr-account flows. ECDSA flows unaffected. ## Security review summary Two specific concerns scrutinized: 1. **Limb range enforcement.** noir's `EmbeddedCurveScalar::new(lo, hi)` does no checks; soundness depends on `multi_scalar_mul` (foreign-dispatched to barretenberg's `to_grumpkin_scalar → cycle_scalar(lo, hi)`) enforcing `lo < 2^128`, `hi < 2^126`, and `lo + hi·2^128 < grumpkin_scalar_modulus`. Confirmed: range constraints applied via `create_limbed_range_constraint` inside `cycle_group::batch_mul` (both Straus and constant-infinity paths), with `validate_scalar_is_in_field` enforcing the modulus bound. No forgery possible. There is a benign non-uniqueness — at most two integer representations in `[0, bb::fq_modulus)` can reduce to the same `bb::fr` residue — but auth witness nullifiers key off `message_hash` not the signature payload, so this is not exploitable. 2. **Test vector cross-check.** Ran cpp `pinned_test_vector_{small,large}` and noir-lang/schnorr v0.3.0 `pinned_vector_{small,large}` against the same `(private_key, message)` inputs. Cpp produces `s` / `e` byte-for-byte matching the noir test's `(sig_s.hi, sig_s.lo)` / `(sig_e.hi, sig_e.lo)` limbs, and the noir verifier accepts the signatures in-circuit. ## Breaking change Existing schnorr accounts on testnet cannot be signed for by the new TS code — both the challenge hash function and the authwit wire format change. Users must migrate to a freshly-deployed schnorr account. ECDSA accounts unaffected. --- ...est_chonk_standalone_vks_havent_changed.sh | 2 +- .../src/barretenberg/bbapi/bbapi_schnorr.cpp | 7 +- .../src/barretenberg/bbapi/bbapi_schnorr.hpp | 19 +- .../crypto/schnorr/CMakeLists.txt | 2 +- .../barretenberg/crypto/schnorr/schnorr.hpp | 33 +- .../barretenberg/crypto/schnorr/schnorr.tcc | 177 ++++----- .../crypto/schnorr/schnorr.test.cpp | 354 ++++++++++++------ barretenberg/rust/tests/src/ffi/schnorr.rs | 44 ++- .../docs/resources/migration_notes.md | 28 ++ .../schnorr_account_contract/Nargo.toml | 2 +- .../schnorr_account_contract/src/main.nr | 20 +- .../Nargo.toml | 2 +- .../src/main.nr | 13 +- .../accounts/src/schnorr/account_contract.ts | 4 +- .../schnorr_hardcoded_account_contract.ts | 4 +- .../writing_an_account_contract.test.ts | 4 +- .../src/crypto/schnorr/index.test.ts | 7 +- .../foundation/src/crypto/schnorr/index.ts | 17 +- .../src/crypto/schnorr/signature.ts | 63 +--- yarn-project/stdlib/src/tests/factories.ts | 10 - .../oracle/txe_oracle_top_level_context.ts | 4 +- 21 files changed, 472 insertions(+), 344 deletions(-) diff --git a/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh b/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh index 208d4f26f5c9..8308eea435a5 100755 --- a/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh +++ b/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_changed.sh @@ -21,7 +21,7 @@ script_path="$root/barretenberg/cpp/scripts/test_chonk_standalone_vks_havent_cha # - Generate a hash for versioning: sha256sum bb-chonk-inputs.tar.gz # - Upload the compressed results: aws s3 cp bb-chonk-inputs.tar.gz s3://aztec-ci-artifacts/protocol/bb-chonk-inputs-[hash(0:8)].tar.gz # Note: In case of the "Test suite failed to run ... Unexpected token 'with' " error, need to run: docker pull aztecprotocol/build:3.0 -pinned_short_hash="aafbeabe" +pinned_short_hash="63d1c3f0" pinned_chonk_inputs_url="https://aztec-ci-artifacts.s3.us-east-2.amazonaws.com/protocol/bb-chonk-inputs-${pinned_short_hash}.tar.gz" function update_pinned_hash_in_script { diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_schnorr.cpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_schnorr.cpp index c845a1f37cf1..68b51ea2cd8e 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_schnorr.cpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_schnorr.cpp @@ -16,8 +16,7 @@ SchnorrConstructSignature::Response SchnorrConstructSignature::execute(BB_UNUSED grumpkin::g1::affine_element pub_key = grumpkin::g1::one * private_key; crypto::schnorr_key_pair key_pair = { private_key, pub_key }; - std::string message_str(reinterpret_cast(message.data()), message.size()); - auto sig = crypto::schnorr_construct_signature(message_str, key_pair); + auto sig = crypto::schnorr_construct_signature(message_field, key_pair); crypto::secure_erase_bytes(&key_pair.private_key, sizeof(key_pair.private_key)); return { sig.s, sig.e }; @@ -25,11 +24,9 @@ SchnorrConstructSignature::Response SchnorrConstructSignature::execute(BB_UNUSED SchnorrVerifySignature::Response SchnorrVerifySignature::execute(BB_UNUSED BBApiRequest& request) && { - std::string message_str(reinterpret_cast(message.data()), message.size()); crypto::schnorr_signature sig = { s, e }; - bool result = crypto::schnorr_verify_signature( - message_str, public_key, sig); + bool result = crypto::schnorr_verify_signature(message_field, public_key, sig); return { result }; } diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_schnorr.hpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_schnorr.hpp index a8617dbd2c55..e538bba399e7 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_schnorr.hpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_schnorr.hpp @@ -11,9 +11,6 @@ #include "barretenberg/crypto/schnorr/schnorr.hpp" #include "barretenberg/ecc/curves/grumpkin/grumpkin.hpp" #include "barretenberg/serialize/msgpack.hpp" -#include -#include -#include namespace bb::bbapi { @@ -46,16 +43,16 @@ struct SchnorrConstructSignature { struct Response { static constexpr const char MSGPACK_SCHEMA_NAME[] = "SchnorrConstructSignatureResponse"; - std::array s; - std::array e; + grumpkin::fr s; + grumpkin::fr e; SERIALIZATION_FIELDS(s, e); bool operator==(const Response&) const = default; }; - std::vector message; // Variable length + grumpkin::fq message_field; grumpkin::fr private_key; Response execute(BBApiRequest& request) &&; - SERIALIZATION_FIELDS(message, private_key); + SERIALIZATION_FIELDS(message_field, private_key); bool operator==(const SchnorrConstructSignature&) const = default; }; @@ -73,12 +70,12 @@ struct SchnorrVerifySignature { bool operator==(const Response&) const = default; }; - std::vector message; + grumpkin::fq message_field; grumpkin::g1::affine_element public_key; - std::array s; - std::array e; + grumpkin::fr s; + grumpkin::fr e; Response execute(BBApiRequest& request) &&; - SERIALIZATION_FIELDS(message, public_key, s, e); + SERIALIZATION_FIELDS(message_field, public_key, s, e); bool operator==(const SchnorrVerifySignature&) const = default; }; diff --git a/barretenberg/cpp/src/barretenberg/crypto/schnorr/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/crypto/schnorr/CMakeLists.txt index a5d947a980a5..d9bd3dda7c0c 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/schnorr/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/crypto/schnorr/CMakeLists.txt @@ -1 +1 @@ -barretenberg_module(crypto_schnorr crypto_pedersen_hash crypto_blake2s crypto_keccak crypto_sha256 numeric) \ No newline at end of file +barretenberg_module(crypto_schnorr crypto_poseidon2 numeric) \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.hpp b/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.hpp index 171d7dc420af..cb822769421d 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.hpp +++ b/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.hpp @@ -1,13 +1,9 @@ #pragma once -#include #include -#include #include "barretenberg/ecc/curves/grumpkin/grumpkin.hpp" -#include "barretenberg/crypto/hashers/hashers.hpp" - #include "barretenberg/common/serialize.hpp" #include "barretenberg/common/streams.hpp" #include "barretenberg/serialize/msgpack.hpp" @@ -18,29 +14,26 @@ template struct schnorr_key_pair { typename G1::affine_element public_key; }; -// Raw representation of a Schnorr signature (e,s). We use the short variant of Schnorr -// where we include the challenge hash `e` instead of the group element R representing -// the provers initial message. +// Short-Schnorr signature (s, e): include the challenge `e` instead of the group element R. +// +// `s` is the prover's response to the challenge, a scalar in the grumpkin scalar field. +// `e` is the challenge hash. Conceptually a Poseidon2 output (which lives in the grumpkin base +// field = `bb::fr`); since `bb::fr modulus < bb::fq modulus`, every challenge value embeds losslessly +// into the grumpkin scalar field, so we store it as the same scalar type as `s`. struct schnorr_signature { - - // `s` is a serialized field element (also 32 bytes), representing the prover's response to - // to the verifier challenge `e`. - // We do not enforce that `s` is canonical since signatures are verified inside a circuit, - // and are provided as private inputs. Malleability is not an issue in this case. - std::array s; - // `e` represents the verifier's challenge in the protocol. It is encoded as the 32-byte - // output of a hash function modeling a random oracle in the Fiat-Shamir transform. - std::array e; + grumpkin::fr s; + grumpkin::fr e; SERIALIZATION_FIELDS(s, e); }; -template -bool schnorr_verify_signature(const std::string& message, +template +bool schnorr_verify_signature(const typename G1::Fq& message_field, const typename G1::affine_element& public_key, const schnorr_signature& sig); -template -schnorr_signature schnorr_construct_signature(const std::string& message, const schnorr_key_pair& account); +template +schnorr_signature schnorr_construct_signature(const typename G1::Fq& message_field, + const schnorr_key_pair& account); inline bool operator==(schnorr_signature const& lhs, schnorr_signature const& rhs) { diff --git a/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.tcc b/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.tcc index 2c11a78d6ebd..847608a53244 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.tcc +++ b/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.tcc @@ -1,123 +1,111 @@ #pragma once +#include + #include "barretenberg/crypto/hmac/hmac.hpp" -#include "barretenberg/crypto/pedersen_hash/pedersen.hpp" +#include "barretenberg/crypto/poseidon2/poseidon2.hpp" #include "schnorr.hpp" namespace bb::crypto { /** - * @brief Generate the schnorr signature challenge parameter `e` given a message, signer pubkey and nonce + * @brief Domain separation tag (DST) for the schnorr challenge hash. * - * @details Normal Schnorr param e = H(R.x || pubkey || message) - * But we want to keep hash preimage to <= 64 bytes for a 32 byte message - * (for performance reasons in our join-split circuit!) + * Defined as `poseidon2_hash_bytes("schnorr_grumpkin_poseidon2")`, where the byte-packing matches + * noir's `poseidon2_hash_bytes`: bytes are packed little-endian into a single 31-byte chunk + * (positions beyond the source length implicitly zero) and the resulting field element is hashed + * with Poseidon2 (length-1 input, IV = 1 << 64). * - * barretenberg schnorr defines e as the following: + * The same constant is mirrored in noir-lang/schnorr (`SCHNORR_CHALLENGE_DST` in `src/lib.nr`) and + * verified there by a derivation test. Including it as the first input to the challenge hash binds + * every signature to the "schnorr over grumpkin with Poseidon2" scheme, preventing cross-protocol + * reinterpretation of a Poseidon2 output as a schnorr challenge. + */ +template static inline Fq compute_schnorr_challenge_dst() +{ + constexpr std::string_view SRC = "schnorr_grumpkin_poseidon2"; + static_assert(SRC.size() <= 31, "DST source string must fit in a single 31-byte chunk"); + uint256_t packed = 0; + uint256_t mul = 1; + for (char c : SRC) { + packed += uint256_t(static_cast(c)) * mul; + mul *= 256; + } + return Poseidon2::hash({ Fq(packed) }); +} + +/** + * @brief Generate the schnorr signature challenge parameter `e` given a message, signer pubkey and nonce. * - * e = H(pedersen(R.x || pubkey.x || pubkey.y), message) + * @details e = Poseidon2(SCHNORR_CHALLENGE_DST, R.x, pubkey.x, pubkey.y, message_field) * - * pedersen is collision resistant => e can be modelled as randomly distributed - * as long as H can be modelled as a random oracle + * Poseidon2 operates over bb::fr (BN254 scalar field = grumpkin base field). The output is a bb::fr + * element. Since bb::fr modulus < bb::fq modulus (grumpkin scalar field), the result can be converted + * to a grumpkin scalar (bb::fq) with no reduction and zero bias. * - * @tparam Hash the hash-function used as random-oracle - * @tparam G1 Group over which the signature is produced - * @param message what are we signing over? + * @tparam G1 Group over which the signature is produced (grumpkin::g1, where G1::Fq = bb::fr) + * @param message_field The message to sign, as a bb::fr element * @param pubkey the pubkey of the signer - * @param R the nonce - * @return e = H(pedersen(R.x || pubkey.x || pubkey.y), message) as a 256-bit integer, - * represented in a container of 32 uint8_t's - * - * - * @warning When the order of G1 is significantly smaller than 2²⁵⁶−1, - * the distribution of `e` is no longer uniform over `Fr`. This mainly affects - * the ZK property of the scheme. If signatures are never revealed (i.e. if they - * are always private inputs to circuits) then nothing would be revealed anyway. + * @param R the nonce commitment + * @return e as a bb::fr element */ -template -static auto schnorr_generate_challenge(const std::string& message, - const typename G1::affine_element& pubkey, - const typename G1::affine_element& R) +template +static typename G1::Fq schnorr_generate_challenge(const typename G1::Fq& message_field, + const typename G1::affine_element& pubkey, + const typename G1::affine_element& R) { - using Fq = typename G1::Fq; - // create challenge message pedersen_commitment(R.x, pubkey) - Fq compressed_keys = crypto::pedersen_hash::hash({ R.x, pubkey.x, pubkey.y }); - std::vector e_buffer; - write(e_buffer, compressed_keys); - std::copy(message.begin(), message.end(), std::back_inserter(e_buffer)); - - // hash the result of the pedersen hash digest - // we return auto since some hash implementation return - // either a std::vector or a std::array with 32 bytes - return Hash::hash(e_buffer); + static const typename G1::Fq dst = compute_schnorr_challenge_dst(); + return Poseidon2::hash({ dst, R.x, pubkey.x, pubkey.y, message_field }); } /** - * @brief Construct a Schnorr signature of the form (random - priv * hash, hash) using the group G1. + * @brief Construct a Schnorr signature (s, e) where s = k - priv * e and e is the challenge hash. * - * @warning Proofs are not deterministic. - * - * @tparam Hash: A function std::vector -> std::array - * @tparam Fq: The field over which points of G1 are defined. - * @tparam Fr: A class with a random element generator, where the multiplication - * G1::one * k is defined for any randomly-generated class member. - * @tparam G1: A group with a generator G1:one, where an element R is assumed - * to posses an 'x-coordinate' R.x lying in the field Fq. It is also assumed that - * G1 comes with a notion of an 'affine element'. - * @param message A standard library string reference. - * @param account A private key-public key pair in Fr × {affine elements of G1}. - * @return signature + * @warning Signatures are not deterministic (nonce k is random). */ -template -schnorr_signature schnorr_construct_signature(const std::string& message, const schnorr_key_pair& account) +template +schnorr_signature schnorr_construct_signature(const typename G1::Fq& message_field, + const schnorr_key_pair& account) { - // sanity check to ensure our hash function produces `e_raw` - // of exactly 32 bytes. - static_assert(Hash::OUTPUT_SIZE == 32); - auto& public_key = account.public_key; auto& private_key = account.private_key; - // sample random nonce k + // Sample random nonce k. // - // Fr::random_element() will call std::random_device, which in turn relies on system calls to generate a string - // of random bits. It is important to ensure that the execution environment will correctly supply system calls - // that give std::random_device access to an entropy source that produces a string of non-deterministic - // uniformly random bits. For example, when compiling into a wasm binary, it is essential that the random_get - // method is overloaded to utilise a suitable entropy source - // (see https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md) + // Fr::random_element() will call std::random_device, which in turn relies on system calls to + // generate a string of random bits. It is important to ensure that the execution environment will + // correctly supply system calls that give std::random_device access to an entropy source that + // produces a string of non-deterministic uniformly random bits. For example, when compiling into a + // wasm binary, it is essential that the random_get method is overloaded to utilise a suitable + // entropy source (see https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md). // + // Reuse of k across two distinct messages signed by the same private key reveals the private key, + // so a working entropy source is load-bearing for security, not just liveness. Fr k = Fr::random_element(); // k is a secret nonce; use the constant-time multiplication to defend against the // Hamming-weight / bit-length timing leak in operator*. typename G1::affine_element R(typename G1::element(G1::one).mul_const_time(k)); - auto e_raw = schnorr_generate_challenge(message, public_key, R); - // the conversion from e_raw results in a biased field element e - Fr e = Fr::serialize_from_buffer(&e_raw[0]); - Fr s = k - (private_key * e); + using Fq = typename G1::Fq; + Fq e = schnorr_generate_challenge(message_field, public_key, R); + // Convert the challenge from bb::fr (Poseidon2 output field, grumpkin base) to bb::fq (grumpkin scalar) + // for the signature equation s = k - priv * e. Lossless because bb::fr modulus < bb::fq modulus. + std::array e_buf; + Fq::serialize_to_buffer(e, e_buf.data()); + Fr e_scalar = Fr::serialize_from_buffer(e_buf.data()); + Fr s = k - (private_key * e_scalar); secure_erase_bytes(&k, sizeof(k)); - // we serialize e_raw rather than e, so that no binary conversion needs to be - // performed during verification. - // indeed, e_raw defines an integer exponent which exponentiates the public_key point. - // if we define e_uint as the integers whose binary representation is e_raw, - // and e = e_uint % r, where r is the order of the curve, - // and pk as the point representing the public_key, - // then e•pk = e_uint•pk - schnorr_signature sig; - Fr::serialize_to_buffer(s, &sig.s[0]); - std::copy(e_raw.begin(), e_raw.end(), sig.e.begin()); - return sig; + return { s, e_scalar }; } /** - * @brief Verify a Schnorr signature of the sort produced by schnorr_construct_signature. + * @brief Verify a Schnorr signature produced by schnorr_construct_signature. */ -template -bool schnorr_verify_signature(const std::string& message, +template +bool schnorr_verify_signature(const typename G1::Fq& message_field, const typename G1::affine_element& public_key, const schnorr_signature& sig) { @@ -127,32 +115,23 @@ bool schnorr_verify_signature(const std::string& message, if (!public_key.on_curve() || public_key.is_point_at_infinity()) { return false; } - // Deserializing from a 256-bit buffer will induce a bias on the order of - // 1/(2(256-log(r))) where r is the order of Fr, since we perform a modular reduction - Fr e = Fr::serialize_from_buffer(&sig.e[0]); - - // reading s in this way always applies the modular reduction, and - // therefore a signature where (r,s') where s'=s+Fr::modulus would also be accepted - // this makes our signatures malleable, but is not an issue in the context of the - // circuits where we use these signatures - Fr s = Fr::serialize_from_buffer(&sig.s[0]); - if (s == 0 || e == 0) { + if (sig.s == 0 || sig.e == 0) { return false; } - // R = g^{sig.s} • pub^{sig.e} - affine_element R(element(public_key) * e + G1::one * s); + // R = g^s * pub^e + affine_element R(element(public_key) * sig.e + G1::one * sig.s); if (R.is_point_at_infinity()) { - // this result implies k == 0, which would be catastrophic for the prover. - // it is a cheap check that ensures this doesn't happen. return false; } - // compare the _hashes_ rather than field elements modulo r - - // e = H(pedersen(r, pk.x, pk.y), m), where r = x(R) - auto target_e = schnorr_generate_challenge(message, public_key, R); - return std::equal(sig.e.begin(), sig.e.end(), target_e.begin(), target_e.end()); + // Recompute challenge and compare + using Fq = typename G1::Fq; + Fq target_e = schnorr_generate_challenge(message_field, public_key, R); + std::array target_e_buf; + Fq::serialize_to_buffer(target_e, target_e_buf.data()); + Fr target_e_scalar = Fr::serialize_from_buffer(target_e_buf.data()); + return (sig.e == target_e_scalar); } } // namespace bb::crypto diff --git a/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.test.cpp b/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.test.cpp index 6946053a4a88..bcc101dd6314 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.test.cpp +++ b/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.test.cpp @@ -1,139 +1,281 @@ #include "schnorr.hpp" +#include "barretenberg/common/log.hpp" +#include "barretenberg/crypto/poseidon2/poseidon2.hpp" #include "barretenberg/ecc/curves/grumpkin/grumpkin.hpp" #include using namespace bb; using namespace bb::crypto; -crypto::schnorr_key_pair generate_signature() +using Fr = grumpkin::fr; +using Fq = grumpkin::fq; +using G1 = grumpkin::g1; + +TEST(schnorr, verify_signature) +{ + schnorr_key_pair account; + account.private_key = Fr::random_element(); + account.public_key = G1::one * account.private_key; + + Fq message_field = Fq::random_element(); + + auto sig = schnorr_construct_signature(message_field, account); + bool result = schnorr_verify_signature(message_field, account.public_key, sig); + + EXPECT_TRUE(result); +} + +TEST(schnorr, verify_signature_failure_wrong_message) +{ + schnorr_key_pair account; + account.private_key = Fr::random_element(); + account.public_key = G1::one * account.private_key; + + Fq message_field = Fq::random_element(); + Fq wrong_message = Fq::random_element(); + + auto sig = schnorr_construct_signature(message_field, account); + bool result = schnorr_verify_signature(wrong_message, account.public_key, sig); + + EXPECT_FALSE(result); +} + +TEST(schnorr, verify_signature_failure_wrong_key) { - crypto::schnorr_key_pair account; - account.private_key = grumpkin::fr::random_element(); - account.public_key = grumpkin::g1::one * account.private_key; - return account; + schnorr_key_pair account; + account.private_key = Fr::random_element(); + account.public_key = G1::one * account.private_key; + + Fq message_field = Fq::random_element(); + + auto sig = schnorr_construct_signature(message_field, account); + + auto wrong_key = G1::affine_element(G1::one * Fr::random_element()); + bool result = schnorr_verify_signature(message_field, wrong_key, sig); + + EXPECT_FALSE(result); } -TEST(schnorr, verify_signature_keccak256) +TEST(schnorr, signatures_not_deterministic) { - std::string message = "The quick brown fox jumped over the lazy dog."; + schnorr_key_pair account; + account.private_key = Fr::random_element(); + account.public_key = G1::one * account.private_key; - crypto::schnorr_key_pair account; - account.private_key = grumpkin::fr::random_element(); - account.public_key = grumpkin::g1::one * account.private_key; + Fq message_field = Fq::random_element(); - crypto::schnorr_signature signature = - crypto::schnorr_construct_signature(message, account); + auto sig_a = schnorr_construct_signature(message_field, account); + auto sig_b = schnorr_construct_signature(message_field, account); - bool result = crypto::schnorr_verify_signature( - message, account.public_key, signature); + // Different nonces should produce different signatures + EXPECT_NE(sig_a.e, sig_b.e); + EXPECT_NE(sig_a.s, sig_b.s); - EXPECT_EQ(result, true); + // But both should verify + bool result_a = schnorr_verify_signature(message_field, account.public_key, sig_a); + EXPECT_TRUE(result_a); + bool result_b = schnorr_verify_signature(message_field, account.public_key, sig_b); + EXPECT_TRUE(result_b); } -TEST(schnorr, verify_signature_sha256) +/** + * @brief Verify the signature internals independently, without relying on construct + verify using the same code path. + * + * This test manually recomputes the Poseidon2 challenge and checks the Schnorr equation s = k - priv * e, + * catching bugs like the reinterpret_cast issue where both sides had a matching bug that cancelled out. + */ +TEST(schnorr, signature_internals_consistency) { - std::string message = "The quick brown dog jumped over the lazy fox."; + // Use a fixed private key for reproducibility + Fr private_key = Fr(12345); + G1::affine_element public_key(G1::one * private_key); + schnorr_key_pair account = { private_key, public_key }; + + Fq message_field = Fq(67890); - crypto::schnorr_key_pair account; - account.private_key = grumpkin::fr::random_element(); - account.public_key = grumpkin::g1::one * account.private_key; + auto sig = schnorr_construct_signature(message_field, account); - crypto::schnorr_signature signature = - crypto::schnorr_construct_signature(message, account); + // Reconstruct R = g^s * pub^e (this is what the verifier does) + G1::affine_element R(G1::element(public_key) * sig.e + G1::one * sig.s); - bool result = crypto::schnorr_verify_signature( - message, account.public_key, signature); + // Independently compute the Poseidon2 challenge from DST, R, pubkey, message + Fq expected_e_base = Poseidon2::hash( + { compute_schnorr_challenge_dst(), R.x, public_key.x, public_key.y, message_field }); - EXPECT_EQ(result, true); + // Convert to the grumpkin scalar field via byte round-trip (the same path the implementation uses) + std::array expected_e_buf; + Fq::serialize_to_buffer(expected_e_base, expected_e_buf.data()); + Fr expected_e_scalar = Fr::serialize_from_buffer(expected_e_buf.data()); + + // The challenge in the signature must match the independently computed one + EXPECT_EQ(sig.e, expected_e_scalar); + + // Also verify that R is not the point at infinity (would indicate k=0) + EXPECT_FALSE(R.is_point_at_infinity()); +} + +/** + * @brief Pin the schnorr challenge domain separator. + * + * Cross-implementation regression: the value here must match `SCHNORR_CHALLENGE_DST` in + * noir-lang/schnorr (`src/lib.nr`). Both sides derive the same constant from + * `poseidon2_hash_bytes("schnorr_grumpkin_poseidon2")`; if either side drifts, every signature + * stops verifying across the cpp signer / noir verifier boundary. + */ +TEST(schnorr, challenge_dst_pinned) +{ + Fq derived = compute_schnorr_challenge_dst(); + EXPECT_EQ(derived, Fq(std::string("0x024c76938ed06b8ec1d9094b1013d190baa4011372f0604643bda812a63b832e"))); } -TEST(schnorr, verify_signature_blake2s) +/** + * @brief Verify that the cross-field serialization round-trip is lossless. + * + * Since Fr (Grumpkin scalar = BN254 Fq) has a larger modulus than Fq (Grumpkin base = BN254 Fr), + * every Fq value should survive the Fq -> bytes -> Fr conversion without loss. + */ +TEST(schnorr, cross_field_serialization_is_lossless) { - std::string message = "The quick brown dog jumped over the lazy fox."; + for (int i = 0; i < 100; i++) { + // Generate a random Fq element (BN254 Fr, the Poseidon2 output field) + Fq original = Fq::random_element(); - crypto::schnorr_key_pair account; - // account.private_key = grumpkin::fr::random_element(); - account.private_key = { 0x55555555, 0x55555555, 0x55555555, 0x55555555 }; - account.public_key = grumpkin::g1::one * account.private_key; + // Serialize to bytes + std::array buf; + Fq::serialize_to_buffer(original, buf.data()); - crypto::schnorr_signature signature = - crypto::schnorr_construct_signature(message, account); + // Deserialize as Fr (BN254 Fq, the Grumpkin scalar field) + Fr converted = Fr::serialize_from_buffer(buf.data()); - bool result = crypto::schnorr_verify_signature( - message, account.public_key, signature); + // Serialize Fr back to bytes + std::array buf2; + Fr::serialize_to_buffer(converted, buf2.data()); - EXPECT_EQ(result, true); + // The byte representations must be identical (no information lost in the conversion) + EXPECT_EQ(buf, buf2); + } } -TEST(schnorr, hmac_signature_consistency) +/** + * @brief Verify that the bbapi byte interface produces valid signatures. + * + * Simulates the bbapi path: message comes as 32 bytes (a serialized field element), + * gets deserialized to Fq, used for signing, then verified. + */ +TEST(schnorr, bbapi_byte_interface_round_trip) { - std::string message_a = "The quick brown fox jumped over the lazy dog."; - std::string message_b = "The quick brown dog jumped over the lazy fox."; - - auto account_a = generate_signature(); - auto account_b = generate_signature(); - - ASSERT_NE(account_a.private_key, account_b.private_key); - ASSERT_NE(account_a.public_key, account_b.public_key); - - // k is no longer identical, so signatures should be different. - auto signature_a = - schnorr_construct_signature(message_a, account_a); - auto signature_b = - schnorr_construct_signature(message_a, account_a); - - ASSERT_NE(signature_a.e, signature_b.e); - ASSERT_NE(signature_a.s, signature_b.s); - - // same message, different accounts should give different sigs! - auto signature_c = - schnorr_construct_signature(message_a, account_a); - auto signature_d = - schnorr_construct_signature(message_a, account_b); - - ASSERT_NE(signature_c.e, signature_d.e); - ASSERT_NE(signature_c.s, signature_d.s); - - // different message, same accounts should give different sigs! - auto signature_e = - schnorr_construct_signature(message_a, account_a); - auto signature_f = - schnorr_construct_signature(message_b, account_a); - - ASSERT_NE(signature_e.e, signature_f.e); - ASSERT_NE(signature_e.s, signature_f.s); - - // different message, different accounts should give different sigs!! - auto signature_g = - schnorr_construct_signature(message_a, account_a); - auto signature_h = - schnorr_construct_signature(message_b, account_b); - - ASSERT_NE(signature_g.e, signature_h.e); - ASSERT_NE(signature_g.s, signature_h.s); - - bool res = schnorr_verify_signature( - message_a, account_a.public_key, signature_a); - EXPECT_EQ(res, true); - res = schnorr_verify_signature( - message_a, account_a.public_key, signature_b); - EXPECT_EQ(res, true); - res = schnorr_verify_signature( - message_a, account_a.public_key, signature_c); - EXPECT_EQ(res, true); - res = schnorr_verify_signature( - message_a, account_b.public_key, signature_d); - EXPECT_EQ(res, true); - res = schnorr_verify_signature( - message_a, account_a.public_key, signature_e); - EXPECT_EQ(res, true); - res = schnorr_verify_signature( - message_b, account_a.public_key, signature_f); - EXPECT_EQ(res, true); - res = schnorr_verify_signature( - message_a, account_a.public_key, signature_g); - EXPECT_EQ(res, true); - res = schnorr_verify_signature( - message_b, account_b.public_key, signature_h); - EXPECT_EQ(res, true); + Fr private_key = Fr::random_element(); + G1::affine_element public_key(G1::one * private_key); + schnorr_key_pair account = { private_key, public_key }; + + // Simulate bbapi: start from a field element, serialize to bytes, then deserialize + Fq original_message = Fq::random_element(); + std::array message_bytes; + Fq::serialize_to_buffer(original_message, message_bytes.data()); + + // This is what bbapi does + Fq deserialized_message = Fq::serialize_from_buffer(message_bytes.data()); + EXPECT_EQ(original_message, deserialized_message); + + // Sign with deserialized message, verify with original — must agree + auto sig = schnorr_construct_signature(deserialized_message, account); + bool result = schnorr_verify_signature(original_message, public_key, sig); + EXPECT_TRUE(result); +} + +namespace { + +/** + * @brief Build a fully-deterministic Schnorr signature with a caller-supplied nonce k. Mirrors + * schnorr_construct_signature exactly but exposes (R, e_base, e_scalar, s) for pinning. + * + * Used to produce hardcoded test vectors that the noir circuit can match against. + */ +struct DeterministicSig { + G1::affine_element public_key; + G1::affine_element R; + Fq e_base; // raw Poseidon2 output, lives in the grumpkin base field (bb::fr) + Fr e_scalar; // re-encoded into the grumpkin scalar field (bb::fq) for the signature equation + Fr s; // s = k - priv * e_scalar + schnorr_signature sig; +}; + +DeterministicSig build_deterministic_sig(const Fr& private_key, const Fr& nonce_k, const Fq& message_field) +{ + DeterministicSig out; + out.public_key = G1::affine_element(G1::one * private_key); + out.R = G1::affine_element(G1::one * nonce_k); + out.e_base = schnorr_generate_challenge(message_field, out.public_key, out.R); + std::array e_buf; + Fq::serialize_to_buffer(out.e_base, e_buf.data()); + out.e_scalar = Fr::serialize_from_buffer(e_buf.data()); + out.s = nonce_k - private_key * out.e_scalar; + out.sig.s = out.s; + out.sig.e = out.e_scalar; + return out; +} + +void dump_vector(const char* label, const Fr& private_key, const Fr& nonce_k, const Fq& message_field) +{ + auto v = build_deterministic_sig(private_key, nonce_k, message_field); + info("=== ", label, " ==="); + info(" private_key = ", private_key); + info(" nonce_k = ", nonce_k); + info(" message = ", message_field); + info(" public_key.x = ", v.public_key.x); + info(" public_key.y = ", v.public_key.y); + info(" R.x = ", v.R.x); + info(" R.y = ", v.R.y); + info(" s = ", v.s); + info(" e = ", v.e_scalar); +} + +} // namespace + +/** + * @brief Pinned test vector #1: small inputs. + * + * Hardcoded (private_key, nonce_k, message) -> (public_key, signature). The expected outputs are + * pinned; any change to the schnorr scheme (e.g. hash function, challenge ordering) will break this. + * Round-trip verification ensures the recomputed values still satisfy the verifier. + */ +TEST(schnorr, pinned_test_vector_small) +{ + Fr private_key(std::string("0x000000000000000000000000000000000000000000000000000000000000007b")); // 123 + Fr nonce_k(std::string("0x00000000000000000000000000000000000000000000000000000000000001c8")); // 456 + Fq message_field(std::string("0x00000000000000000000000000000000000000000000000000000000000002bc")); // 700 + + auto v = build_deterministic_sig(private_key, nonce_k, message_field); + dump_vector("pinned_test_vector_small", private_key, nonce_k, message_field); + + bool ok = schnorr_verify_signature(message_field, v.public_key, v.sig); + EXPECT_TRUE(ok); + + EXPECT_EQ(v.public_key.x, Fq(std::string("0x2c39bbbde2d0ffcb5c4317dcbfa1771cf554a2f33c647446632fa707a5bf5f3f"))); + EXPECT_EQ(v.public_key.y, Fq(std::string("0x2b9c81935298af5ebe22f1a7279bb76781e6cadba3fb6c5c41ed942392dc687c"))); + EXPECT_EQ(v.R.x, Fq(std::string("0x2f410c5089a00d9a4664f262272dbc091b121acf58abdf919c1bc8b974fb720e"))); + EXPECT_EQ(v.s, Fr(std::string("0x281906862cdb4e0efec7226d757fe8035fd1ac0ad411110674830c54cb506212"))); + EXPECT_EQ(v.e_scalar, Fr(std::string("0x013f6a902c6c0efafdadbd4de409690d6c368959f958e525d761d06c47fd2ad6"))); +} + +/** + * @brief Pinned test vector #2: random-looking large inputs. + */ +TEST(schnorr, pinned_test_vector_large) +{ + Fr private_key(std::string("0x1f2e3d4c5b6a79880f1e2d3c4b5a69788f9e0d1c2b3a4958e7f6d5c4b3a29180")); + Fr nonce_k(std::string("0x2a1b3c4d5e6f78890a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071")); + Fq message_field(std::string("0x0123456789abcdef0fedcba9876543210123456789abcdef0fedcba987654321")); + + auto v = build_deterministic_sig(private_key, nonce_k, message_field); + dump_vector("pinned_test_vector_large", private_key, nonce_k, message_field); + + bool ok = schnorr_verify_signature(message_field, v.public_key, v.sig); + EXPECT_TRUE(ok); + + EXPECT_EQ(v.public_key.x, Fq(std::string("0x065812e335a97c2108ea8cf4ccfe2f9dd6b117a0714f5e18461575be93f61da6"))); + EXPECT_EQ(v.public_key.y, Fq(std::string("0x1a915003e8ec534f9a15d926a7ded478e178468ccc4f28e236e67450a55ac622"))); + EXPECT_EQ(v.R.x, Fq(std::string("0x04e780bc3d2b86b5f41f3b8d2820c1f2c3164cd5efc607cd48c428495a8f47b7"))); + EXPECT_EQ(v.s, Fr(std::string("0x08599f379f0301dfefdbd0272554454df3bc3b7147acb9c621fd9f72dbf15ffa"))); + EXPECT_EQ(v.e_scalar, Fr(std::string("0x2ceaee87f45b7a417f0ffb05451a8c9297065383ebbbd76620398792bd259bc2"))); } diff --git a/barretenberg/rust/tests/src/ffi/schnorr.rs b/barretenberg/rust/tests/src/ffi/schnorr.rs index ea8086fd617f..b31f662339fd 100644 --- a/barretenberg/rust/tests/src/ffi/schnorr.rs +++ b/barretenberg/rust/tests/src/ffi/schnorr.rs @@ -104,12 +104,16 @@ fn test_schnorr_sign_and_verify() { .schnorr_compute_public_key(&private_key) .expect("schnorr_compute_public_key failed"); - // Message (arbitrary bytes) - let message = b"Test message for Schnorr signature"; + // Message: a 32-byte big-endian serialized grumpkin base-field element (bbapi asserts size == 32). + let message: [u8; 32] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0xbc, + ]; // Sign let sign_response = api - .schnorr_construct_signature(message, &private_key) + .schnorr_construct_signature(&message, &private_key) .expect("schnorr_construct_signature failed"); // Signature should have s and e components (32 bytes each) @@ -118,7 +122,12 @@ fn test_schnorr_sign_and_verify() { // Verify let verify_response = api - .schnorr_verify_signature(message, pub_key_response.public_key.clone(), &sign_response.s, &sign_response.e) + .schnorr_verify_signature( + &message, + pub_key_response.public_key.clone(), + &sign_response.s, + &sign_response.e, + ) .expect("schnorr_verify_signature failed"); assert!(verify_response.verified, "Signature should be valid"); @@ -141,20 +150,37 @@ fn test_schnorr_verify_wrong_message() { .schnorr_compute_public_key(&private_key) .expect("schnorr_compute_public_key failed"); - let message1 = b"Original message"; - let message2 = b"Different message"; + // Two distinct 32-byte serialized field elements (bbapi asserts size == 32). + let message1: [u8; 32] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x11, + ]; + let message2: [u8; 32] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x22, + ]; // Sign with message1 let sign_response = api - .schnorr_construct_signature(message1, &private_key) + .schnorr_construct_signature(&message1, &private_key) .expect("schnorr_construct_signature failed"); // Verify with message2 - should fail let verify_response = api - .schnorr_verify_signature(message2, pub_key_response.public_key.clone(), &sign_response.s, &sign_response.e) + .schnorr_verify_signature( + &message2, + pub_key_response.public_key.clone(), + &sign_response.s, + &sign_response.e, + ) .expect("schnorr_verify_signature failed"); - assert!(!verify_response.verified, "Signature should be invalid for wrong message"); + assert!( + !verify_response.verified, + "Signature should be invalid for wrong message" + ); api.destroy().expect("Failed to destroy backend"); } diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 1458fba95b12..21ccc8d9e945 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,34 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [bb.js / accounts / aztec.nr] Schnorr signatures switched to Poseidon2 + +The Schnorr challenge hash function changed from `blake2s(pedersen(R.x, pubkey.x, pubkey.y) ‖ message)` to `Poseidon2(DST, R.x, pubkey.x, pubkey.y, message)`, where `DST = poseidon2_hash_bytes("schnorr_grumpkin_poseidon2")` is a domain separation tag binding signatures to this scheme. The change applies end-to-end across the native signer (`bb`), `@aztec/bb.js`, the noir verifier library (`noir-lang/schnorr` v0.2.0 → v0.4.0), and both standard Schnorr account contracts. The auth witness on-wire shape also changes from `[u8; 64]` (the serialized `(s, e)` bytes) to `[Field; 4]` (`[s.lo, s.hi, e.lo, e.hi]`, each scalar split into two 128-bit limbs). + +**Impact:** A previously-deployed Schnorr account cannot be controlled by the new TypeScript code. Both the signature scheme and the auth witness format change, so signatures produced by the new code will fail in-circuit verification against the old account contract, and old-style 64-byte auth witnesses will not decode in the new contract. Users with existing Schnorr accounts on testnet must deploy a fresh account contract and migrate funds. ECDSA accounts (`ecdsa_k`, `ecdsa_r`) are unaffected. + +**If you maintain a custom Schnorr account contract**, bump the `schnorr` dependency in `Nargo.toml`: + +```diff +- schnorr = { tag = "v0.2.0", git = "https://github.com/noir-lang/schnorr" } ++ schnorr = { tag = "v0.4.0", git = "https://github.com/noir-lang/schnorr" } +``` + +and update `is_valid_impl` to consume the auth witness as four `Field` limbs and pass `outer_hash` directly: + +```diff +- let signature: [u8; 64] = unsafe { get_auth_witness_as_bytes(outer_hash) }; +- schnorr::verify_signature(pub_key, signature, outer_hash.to_be_bytes::<32>()) ++ let limbs: [Field; 4] = unsafe { get_auth_witness(outer_hash) }; ++ let signature = ( ++ std::embedded_curve_ops::EmbeddedCurveScalar::new(limbs[0], limbs[1]), ++ std::embedded_curve_ops::EmbeddedCurveScalar::new(limbs[2], limbs[3]), ++ ); ++ schnorr::verify_signature(pub_key, signature, outer_hash) +``` + +The `Schnorr` TypeScript API in `@aztec/foundation/crypto/schnorr` keeps the same surface (`constructSignature(msg: Uint8Array, ...)`, `verifySignature(msg, ...)`), but the `msg` parameter is now required to be exactly 32 bytes — a serialized field element (e.g. `Fr.toBuffer()` or `messageHash.toBuffer()`). Passing arbitrary-length byte strings will fail at the bb.js boundary. + ### [Aztec.nr] `attempt_note_discovery` is no longer exposed; use `process_private_note_msg` `attempt_note_discovery` is now crate-private. Custom message handlers (implementations of `CustomMessageHandler`) that previously called it directly should call `process_private_note_msg` instead, which runs the standard private note message decoding and discovery pipeline. diff --git a/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/Nargo.toml index 4ef03aed2d98..7a8aa9286aba 100644 --- a/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/Nargo.toml @@ -6,4 +6,4 @@ type = "contract" [dependencies] aztec = { path = "../../../../aztec-nr/aztec" } -schnorr = { tag = "v0.2.0", git = "https://github.com/noir-lang/schnorr" } +schnorr = { tag = "v0.4.0", git = "https://github.com/noir-lang/schnorr" } diff --git a/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr index e41ec43ee40f..dff7facab5db 100644 --- a/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/account/schnorr_account_contract/src/main.nr @@ -17,8 +17,8 @@ pub contract SchnorrAccount { macros::{functions::{allow_phase_change, external, initializer, noinitcheck, view}, storage::storage}, messages::message_delivery::MessageDelivery, oracle::{ - auth_witness::get_auth_witness_as_bytes, - get_nullifier_membership_witness::get_low_nullifier_membership_witness, notes::set_sender_for_tags, + auth_witness::get_auth_witness, get_nullifier_membership_witness::get_low_nullifier_membership_witness, + notes::set_sender_for_tags, }, protocol::address::AztecAddress, state_vars::SinglePrivateImmutable, @@ -76,11 +76,15 @@ pub contract SchnorrAccount { // Safety: The witness is only used as a "magical value" that makes the signature verification below pass. // Hence it's safe. - let signature: [u8; 64] = unsafe { get_auth_witness_as_bytes(outer_hash) }; + let limbs: [Field; 4] = unsafe { get_auth_witness(outer_hash) }; + let signature = ( + std::embedded_curve_ops::EmbeddedCurveScalar::new(limbs[0], limbs[1]), + std::embedded_curve_ops::EmbeddedCurveScalar::new(limbs[2], limbs[3]), + ); let pub_key = std::embedded_curve_ops::EmbeddedCurvePoint { x: public_key.x, y: public_key.y }; // Verify signature of the payload bytes - schnorr::verify_signature(pub_key, signature, outer_hash.to_be_bytes::<32>()) + schnorr::verify_signature(pub_key, signature, outer_hash) // docs:end:is_valid_impl } @@ -99,9 +103,13 @@ pub contract SchnorrAccount { inner_hash, ); - let signature: [u8; 64] = get_auth_witness_as_bytes(message_hash); + let limbs: [Field; 4] = get_auth_witness(message_hash); + let signature = ( + std::embedded_curve_ops::EmbeddedCurveScalar::new(limbs[0], limbs[1]), + std::embedded_curve_ops::EmbeddedCurveScalar::new(limbs[2], limbs[3]), + ); let pub_key = std::embedded_curve_ops::EmbeddedCurvePoint { x: public_key.x, y: public_key.y }; - let valid_in_private = schnorr::verify_signature(pub_key, signature, message_hash.to_be_bytes::<32>()); + let valid_in_private = schnorr::verify_signature(pub_key, signature, message_hash); // Compute the nullifier and check if it is spent // This will BLINDLY TRUST the oracle, but the oracle is us, and diff --git a/noir-projects/noir-contracts/contracts/account/schnorr_hardcoded_account_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/account/schnorr_hardcoded_account_contract/Nargo.toml index dd7202a458c5..5875ebc70171 100644 --- a/noir-projects/noir-contracts/contracts/account/schnorr_hardcoded_account_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/account/schnorr_hardcoded_account_contract/Nargo.toml @@ -6,4 +6,4 @@ type = "contract" [dependencies] aztec = { path = "../../../../aztec-nr/aztec" } -schnorr = { tag = "v0.2.0", git = "https://github.com/noir-lang/schnorr" } +schnorr = { tag = "v0.4.0", git = "https://github.com/noir-lang/schnorr" } diff --git a/noir-projects/noir-contracts/contracts/account/schnorr_hardcoded_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/schnorr_hardcoded_account_contract/src/main.nr index 489f87d7fb94..fecb82409408 100644 --- a/noir-projects/noir-contracts/contracts/account/schnorr_hardcoded_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/account/schnorr_hardcoded_account_contract/src/main.nr @@ -7,9 +7,9 @@ pub contract SchnorrHardcodedAccount { authwit::{account::AccountActions, entrypoint::app::AppPayload}, context::PrivateContext, macros::functions::{allow_phase_change, external, view}, - oracle::auth_witness::get_auth_witness_as_bytes, + oracle::auth_witness::get_auth_witness, }; - use std::embedded_curve_ops::EmbeddedCurvePoint; + use std::embedded_curve_ops::{EmbeddedCurvePoint, EmbeddedCurveScalar}; global signing_public_key: EmbeddedCurvePoint = EmbeddedCurvePoint { x: 0x16b93f4afae55cab8507baeb8e7ab4de80f5ab1e9e1f5149bf8cd0d375451d90, @@ -36,13 +36,10 @@ pub contract SchnorrHardcodedAccount { fn is_valid_impl(_context: &mut PrivateContext, outer_hash: Field) -> bool { // Safety: The witness is only used as a "magical value" that makes the signature verification below pass. // Hence it's safe. - let signature: [u8; 64] = unsafe { get_auth_witness_as_bytes(outer_hash) }; + let limbs: [Field; 4] = unsafe { get_auth_witness(outer_hash) }; + let signature = (EmbeddedCurveScalar::new(limbs[0], limbs[1]), EmbeddedCurveScalar::new(limbs[2], limbs[3])); // Verify signature using hardcoded public key - schnorr::verify_signature( - signing_public_key, - signature, - outer_hash.to_be_bytes::<32>(), - ) + schnorr::verify_signature(signing_public_key, signature, outer_hash) } } diff --git a/yarn-project/accounts/src/schnorr/account_contract.ts b/yarn-project/accounts/src/schnorr/account_contract.ts index 5431403bbf16..45fbf9658489 100644 --- a/yarn-project/accounts/src/schnorr/account_contract.ts +++ b/yarn-project/accounts/src/schnorr/account_contract.ts @@ -34,7 +34,7 @@ export class SchnorrAuthWitnessProvider implements AuthWitnessProvider { async createAuthWit(messageHash: Fr): Promise { const schnorr = new Schnorr(); - const signature = await schnorr.constructSignature(messageHash.toBuffer(), this.signingPrivateKey); - return new AuthWitness(messageHash, [...signature.toBuffer()]); + const signature = await schnorr.constructSignature(messageHash, this.signingPrivateKey); + return new AuthWitness(messageHash, signature.toLimbFields()); } } diff --git a/yarn-project/end-to-end/src/fixtures/schnorr_hardcoded_account_contract.ts b/yarn-project/end-to-end/src/fixtures/schnorr_hardcoded_account_contract.ts index 106fb4b30096..0ee5f930930a 100644 --- a/yarn-project/end-to-end/src/fixtures/schnorr_hardcoded_account_contract.ts +++ b/yarn-project/end-to-end/src/fixtures/schnorr_hardcoded_account_contract.ts @@ -41,8 +41,8 @@ export class SchnorrHardcodedKeyAccountContract extends DefaultAccountContract { return { async createAuthWit(messageHash: Fr): Promise { const signer = new Schnorr(); - const signature = await signer.constructSignature(messageHash.toBuffer(), privateKey); - return new AuthWitness(messageHash, [...signature.toBuffer()]); + const signature = await signer.constructSignature(messageHash, privateKey); + return new AuthWitness(messageHash, signature.toLimbFields()); }, }; } diff --git a/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts b/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts index b9f1893bf500..0ed3abf104d7 100644 --- a/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts +++ b/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts @@ -33,8 +33,8 @@ class SchnorrHardcodedKeyAccountContract extends DefaultAccountContract { return { async createAuthWit(messageHash: Fr): Promise { const signer = new Schnorr(); - const signature = await signer.constructSignature(messageHash.toBuffer(), privateKey); - return Promise.resolve(new AuthWitness(messageHash, [...signature.toBuffer()])); + const signature = await signer.constructSignature(messageHash, privateKey); + return Promise.resolve(new AuthWitness(messageHash, signature.toLimbFields())); }, }; } diff --git a/yarn-project/foundation/src/crypto/schnorr/index.test.ts b/yarn-project/foundation/src/crypto/schnorr/index.test.ts index 8a3c1d792e3b..b6b7b2f2446a 100644 --- a/yarn-project/foundation/src/crypto/schnorr/index.test.ts +++ b/yarn-project/foundation/src/crypto/schnorr/index.test.ts @@ -1,7 +1,6 @@ +import { Fr } from '@aztec/foundation/curves/bn254'; import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; -import { TextEncoder } from 'util'; - import { Schnorr } from './index.js'; describe('schnorr', () => { @@ -18,7 +17,7 @@ describe('schnorr', () => { 0xda, 0x31, 0x29, 0x1a, 0x5e, 0x96, 0xbb, 0x7a, 0x56, 0x63, 0x9e, 0x17, 0x7d, 0x30, 0x1b, 0xeb, ])); const pubKey = await schnorr.computePublicKey(privateKey); - const msg = new TextEncoder().encode('The quick brown dog jumped over the lazy fox.'); + const msg = Fr.random(); const signature = await schnorr.constructSignature(msg, privateKey); const verified = await schnorr.verifySignature(msg, pubKey, signature); @@ -32,7 +31,7 @@ describe('schnorr', () => { 0xda, 0x31, 0x29, 0x1a, 0x5e, 0x96, 0xbb, 0x7a, 0x56, 0x63, 0x9e, 0x17, 0x7d, 0x30, 0x1b, 0xeb, ])); const pubKey = await schnorr.computePublicKey(privateKey); - const msg = new TextEncoder().encode('The quick brown dog jumped over the lazy fox.'); + const msg = Fr.random(); const signature = await schnorr.constructSignature(msg, GrumpkinScalar.random()); const verified = await schnorr.verifySignature(msg, pubKey, signature); diff --git a/yarn-project/foundation/src/crypto/schnorr/index.ts b/yarn-project/foundation/src/crypto/schnorr/index.ts index 9ba7f0e3be99..c73173ede5f9 100644 --- a/yarn-project/foundation/src/crypto/schnorr/index.ts +++ b/yarn-project/foundation/src/crypto/schnorr/index.ts @@ -1,4 +1,5 @@ import { BarretenbergSync } from '@aztec/bb.js'; +import type { Fr } from '@aztec/foundation/curves/bn254'; import type { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import { Point } from '@aztec/foundation/curves/grumpkin'; @@ -23,33 +24,33 @@ export class Schnorr { } /** - * Constructs a Schnorr signature given a msg and a private key. - * @param msg - Message over which the signature is constructed. + * Constructs a Schnorr signature over a 32-byte message field element. + * @param msg - The message hash, as a grumpkin base field element. * @param privateKey - The private key of the signer. * @returns A Schnorr signature of the form (s, e). */ - public async constructSignature(msg: Uint8Array, privateKey: GrumpkinScalar) { + public async constructSignature(msg: Fr, privateKey: GrumpkinScalar) { await BarretenbergSync.initSingleton(); const api = BarretenbergSync.getSingleton(); const response = api.schnorrConstructSignature({ - message: msg, + messageField: msg.toBuffer(), privateKey: privateKey.toBuffer(), }); return new SchnorrSignature(Buffer.from([...response.s, ...response.e])); } /** - * Verifies a Schnorr signature given a Grumpkin public key. - * @param msg - Message over which the signature was constructed. + * Verifies a Schnorr signature against a Grumpkin public key. + * @param msg - The message hash, as a grumpkin base field element. * @param pubKey - The Grumpkin public key of the signer. * @param sig - The Schnorr signature. * @returns True or false. */ - public async verifySignature(msg: Uint8Array, pubKey: Point, sig: SchnorrSignature) { + public async verifySignature(msg: Fr, pubKey: Point, sig: SchnorrSignature) { await BarretenbergSync.initSingleton(); const api = BarretenbergSync.getSingleton(); const response = api.schnorrVerifySignature({ - message: msg, + messageField: msg.toBuffer(), publicKey: { x: pubKey.x.toBuffer(), y: pubKey.y.toBuffer() }, s: sig.s, e: sig.e, diff --git a/yarn-project/foundation/src/crypto/schnorr/signature.ts b/yarn-project/foundation/src/crypto/schnorr/signature.ts index 195515e64fde..deb26ad6a9f7 100644 --- a/yarn-project/foundation/src/crypto/schnorr/signature.ts +++ b/yarn-project/foundation/src/crypto/schnorr/signature.ts @@ -1,6 +1,5 @@ -import { randomBytes } from '@aztec/foundation/crypto/random'; import { Fr } from '@aztec/foundation/curves/bn254'; -import { BufferReader, mapTuple } from '@aztec/foundation/serialize'; +import { mapTuple } from '@aztec/foundation/serialize'; import type { Signature } from '../signature/index.js'; @@ -14,46 +13,12 @@ export class SchnorrSignature implements Signature { */ public static SIZE = 64; - /** - * An empty signature. - */ - public static EMPTY = new SchnorrSignature(Buffer.alloc(64)); - constructor(private buffer: Buffer) { if (buffer.length !== SchnorrSignature.SIZE) { throw new Error(`Invalid signature buffer of length ${buffer.length}.`); } } - /** - * Determines if the provided signature is valid or not. - * @param signature - The data to be checked. - * @returns Boolean indicating if the provided data is a valid schnorr signature. - */ - public static isSignature(signature: string) { - return /^(0x)?[0-9a-f]{128}$/i.test(signature); - } - - /** - * Constructs a SchnorrSignature from the provided string. - * @param signature - The string to be converted to a schnorr signature. - * @returns The constructed schnorr signature. - */ - public static fromString(signature: string) { - if (!SchnorrSignature.isSignature(signature)) { - throw new Error(`Invalid signature string: ${signature}`); - } - return new SchnorrSignature(Buffer.from(signature.replace(/^0x/i, ''), 'hex')); - } - - /** - * Generates a random schnorr signature. - * @returns The randomly constructed signature. - */ - public static random() { - return new SchnorrSignature(randomBytes(64)); - } - /** * Returns the 's' component of the signature. * @returns A buffer containing the signature's 's' component. @@ -78,16 +43,6 @@ export class SchnorrSignature implements Signature { return this.buffer; } - /** - * Deserializes from a buffer. - * @param buffer - The buffer representation of the object. - * @returns The new object. - */ - static fromBuffer(buffer: Buffer | BufferReader): SchnorrSignature { - const reader = BufferReader.asReader(buffer); - return new SchnorrSignature(reader.readBytes(SchnorrSignature.SIZE)); - } - /** * Returns the full signature as a hex string. * @returns A string containing the signature in hex format. @@ -113,4 +68,20 @@ export class SchnorrSignature implements Signature { return mapTuple([buf1, buf2, buf3], Fr.fromBuffer); } + + /** + * Splits the signature into the four 128-bit limbs that Noir's `EmbeddedCurveScalar` consumes: + * `[s.lo, s.hi, e.lo, e.hi]`, where each component scalar is encoded as `lo + hi * 2^128`. + * + * Each 32-byte big-endian component is sliced into its top 16 bytes (`hi`) and bottom 16 bytes + * (`lo`); each half is zero-padded into a 32-byte buffer and decoded as `Fr` (big-endian). + */ + toLimbFields(): [Fr, Fr, Fr, Fr] { + const limb = (start: number) => { + const buf = Buffer.alloc(32); + this.buffer.copy(buf, 16, start, start + 16); + return Fr.fromBuffer(buf); + }; + return [limb(16), limb(0), limb(48), limb(32)]; + } } diff --git a/yarn-project/stdlib/src/tests/factories.ts b/yarn-project/stdlib/src/tests/factories.ts index d0cfe8c90bc8..e4a987249fca 100644 --- a/yarn-project/stdlib/src/tests/factories.ts +++ b/yarn-project/stdlib/src/tests/factories.ts @@ -45,7 +45,6 @@ import { type FieldsOf, makeTuple } from '@aztec/foundation/array'; import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { compact } from '@aztec/foundation/collection'; import { Grumpkin } from '@aztec/foundation/crypto/grumpkin'; -import { SchnorrSignature } from '@aztec/foundation/crypto/schnorr'; import { sha256 } from '@aztec/foundation/crypto/sha256'; import { Fq, Fr } from '@aztec/foundation/curves/bn254'; import { GrumpkinScalar, Point } from '@aztec/foundation/curves/grumpkin'; @@ -749,15 +748,6 @@ export function makeAztecAddress(seed = 1): AztecAddress { return AztecAddress.fromField(fr(seed)); } -/** - * Makes arbitrary Schnorr signature. - * @param seed - The seed to use for generating the Schnorr signature. - * @returns A Schnorr signature. - */ -export function makeSchnorrSignature(seed = 1): SchnorrSignature { - return new SchnorrSignature(Buffer.alloc(SchnorrSignature.SIZE, seed)); -} - function makeBlockConstantData(seed = 1, globalVariables?: GlobalVariables) { return new BlockConstantData( makeAppendOnlyTreeSnapshot(seed + 0x100), diff --git a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts index b4908a7663f9..24c1ce0d3239 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts @@ -276,9 +276,9 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl const privateKey = await this.keyStore.getMasterSecretKey(account.publicKeys.masterIncomingViewingPublicKey); const schnorr = new Schnorr(); - const signature = await schnorr.constructSignature(messageHash.toBuffer(), privateKey); + const signature = await schnorr.constructSignature(messageHash, privateKey); - const authWitness = new AuthWitness(messageHash, [...signature.toBuffer()]); + const authWitness = new AuthWitness(messageHash, signature.toLimbFields()); this.authwits.set(authWitness.requestHash.toString(), authWitness); }