This implements BIP458: https://github.com/bitcoin/bips/pull/2205
The original version of this code ported over the work from b-wagn in https://github.com/BlockstreamResearch/secp256k1-zkp/pull/261
This implements BIP458: https://github.com/bitcoin/bips/pull/2205
The original version of this code ported over the work from b-wagn in https://github.com/BlockstreamResearch/secp256k1-zkp/pull/261
Rebased on the musig changes and added the compile options for cmake
Rebased to take into account the latest changes to cmake, CI and test framework. Also squashed the commit since maintaining them was annoying and there wasn't really a good reason for them. It was more of a crutch for me while I was taking my first steps here.
88 | + * Returns: 1: correct signature. 89 | + * 0: incorrect signature. 90 | + * Args: ctx: a secp256k1 context object. 91 | + * In: pubkeys: Array of n many x-only public keys. Can only be NULL if n is 0. 92 | + * msgs32: Array of n many 32-byte messages. Can only be NULL if n is 0. 93 | + * n: number of signatures to that have been aggregated.
* n: number of signatures that have been aggregated.
Thanks, I will address it the next time I will push an update!
Fixed
I think there's a typo
Rebased and added the test vectors from the BIP draft: https://github.com/bitcoin/bips/pull/2205
211 | + secp256k1_ecmult(&lhs, &rhs, &secp256k1_scalar_zero, &s); 212 | + 213 | + /* Check that lhs == rhs */ 214 | + secp256k1_gej_neg(&lhs, &lhs); 215 | + secp256k1_gej_add_var(&lhs, &lhs, &rhs, NULL); 216 | + return secp256k1_gej_is_infinity(&lhs);
could use the secp256k1_gej_eq_var helper here which does the some steps internally:
return secp256k1_gej_eq_var(&lhs, &rhs);
Done, thanks
17 | + sha->s[4] = 0x1d4db069ul; 18 | + sha->s[5] = 0xb4d587e1ul; 19 | + sha->s[6] = 0x50451c2aul; 20 | + sha->s[7] = 0x10fb63e9ul; 21 | + 22 | + sha->bytes = 64;
could use secp256k1_sha256_initialize_midstate here (introduced in PR #1825, commit https://github.com/bitcoin-core/secp256k1/commit/f48b1bfa5d40a4d7303b196017d2e298520d1066), see other modules that use BIP340 tagged hashes
Done, thanks
29 | + secp256k1_scalar s; 30 | + int overflow; 31 | + const secp256k1_hash_ctx *hash_ctx; 32 | + 33 | + VERIFY_CHECK(ctx != NULL); 34 | + hash_ctx = secp256k1_get_hash_context(ctx);
Ah, good catch, fixed and rebased.
11 | @@ -12,6 +12,18 @@ if(SECP256K1_ENABLE_MODULE_ELLSWIFT) 12 | set_property(TARGET secp256k1 APPEND PROPERTY PUBLIC_HEADER ${PROJECT_SOURCE_DIR}/include/secp256k1_ellswift.h) 13 | endif() 14 | 15 | +if(SECP256K1_ENABLE_MODULE_SCHNORRSIG_HALFAGG) 16 | + if(NOT SECP256K1_EXPERIMENTAL) 17 | + message(FATAL_ERROR "Schnorrsig half-aggregation is experimental. Use -DSECP256K1_EXPERIMENTAL=ON to allow.") 18 | + endif()
is the "experimental" category still used for new modules? (cc @real-or-random @sipa) I haven't seen it for the most recent ones (ellswift, musig, silentpayments), but IIUC the schnorrsig had it back then.
Yeah, I myself argued that it might be possible to remove the category at some IRC meeting or the in-person meeting probably about a year ago. This was about the batch module specifically that still had it and it made sense for me to remove it there because it was already in BIP340 and the code would only be merged if it was in a usable state anyway, the bar is just that high already. I think @sipa said the category should be kept around if we want to introduce a module where the API may still change, at least that's the only argument I remember from the conversation. I already considered removing the category here and in fullagg for the same reasons as batch but then reconsidered: Maybe we would want to set it for some code that is specifically added for a soft fork before the soft fork is actually activated? (There are other use cases outside of consensus for halfagg and fullagg but realistically those will not get these PRs merged here.) I also saw that the Schnorr module was also merged as experimental but I did not go the extra mile to ask if this was actually because of the consensus angle, but I did the lazy thing and made no change here :)
However, I just now did some more digging and found this comment from @sipa that clarifies the intention for BIP340 at the time: #817 (comment) The whole issue was actually just closed after we discussed it as a group, btw.
I am currently basically 50/50 because I think the arguments for keeping Schnorr experimental from the comment very much apply to this pull here as is. But on the other hand I think the pull has a very low chance of getting merged while these arguments still apply because of the high standard here and if we agree on that we can remove the status already. Well, there is a chance that an alternative, newly discovered use case becomes strong enough for an earlier merge so maybe 51/49 for keeping it experimental for now and potentially removing it before the merge if doesn't seem needed anymore.
That's a long way of saying, I am happy to remove or keep it around.
160 | + return 0; 161 | + } 162 | + secp256k1_fe_get_b32(pk_ser, &pp.x); 163 | + 164 | + /* Step 1: z_i = TaggedHash(...) */ 165 | + /* 1.a) Write into hash r_i, pk_i, m_i, r_i */
/* 1.a) Write into hash r_i, pk_i, m_i */
Good catch, fixed here and found the same error in inc_aggregate, fixed there as well.
200 | + * no ecmult_gen context, so aggverify works with secp256k1_context_static. */ 201 | + secp256k1_scalar_set_b32(&s, &aggsig[n*32], &overflow); 202 | + if (overflow) { 203 | + return 0; 204 | + } 205 | + secp256k1_ecmult(&lhs, &rhs, &secp256k1_scalar_zero, &s);
not applicable yet, but just leaving a note already that in the future this could be replaced with ecmult_gen_var_gej, if #1883 gets in. for the agg_verify benchmark (with N=3), I'm seeing a ~5% speedup on my machine (95.5us vs. 90.5us): https://github.com/theStack/secp256k1/commit/cbe15b8f2ceaec0c0262d86c427617e0e65eb01d
Nice, I will keep an eye on that!
Changed the tag to include the BIP number
The vector generator breaks on the BIP's new size vectors, and inc_aggregate differs from the BIP when n_before is 0.
I ran the vectors of bitcoin/bips#2205 at 2f436b0 through the module, and 300 random cases against an implementation I maintain, on btclib-ecc. 36 of the 37 vectors the C API can express pass, and the random cases agree byte for byte, including corrupted inputs and s ≥ n. Results are the same on a local merge with master.
Rerunning tools/test_vectors_halfagg_generate.py on that BIP head writes the eight new size vectors into fixed-size arrays. The too-long ones fail the build under -Werror. The too-short ones are zero-padded without a warning, so aggregate vector 9, a 31-byte key, succeeds and the test aborts. The generator could skip them, as it skips incaggregate 4 to 6. The vector left, incaggregate 6, is skipped because inc_aggregate ignores the input s when n_before is 0, while the BIP adds it and fails when it is ≥ n. With s = 1, the C result verifies and the BIP's does not. aggregate relies on this, passing its output buffer as the input. Zeroing those 32 bytes in aggregate, as the BIP's Aggregate does, would let inc_aggregate read s. Otherwise the header could say s is ignored there. I measured through ctypes and can share the script.
3361972502628adaa7938f8473067f190296fc37 needs rebase
Rebased
The vector generator breaks on the BIP's new size vectors
The latest update of the BIP just wasn't pulled in yet. I updated the vectors to the latest version of the BIP. The generator now skips the size vectors.
inc_aggregatediffers from the BIP whenn_beforeis 0
I addressed this. inc_aggregate now always reads s, and aggregate starts from s = 0 through a shared internal helper instead of zeroing the buffer. Vector 6 is also no longer skipped.
Concept ACK
One issue should be fixed before merge: doc/halfagg.md says that aggregating an invalid signature gives an invalid aggregate. That holds for one invalid input, but two can cancel out, as BIP 458 warns under both Aggregate and IncAggregate.
At ac35c4a83d7effb33180a30679dfa3fb1aefffcf, two signatures that each fail secp256k1_schnorrsig_verify aggregate into a signature that secp256k1_schnorrsig_aggverify accepts, in 100 of 100 random cases. I add d to one s and subtract d/z1 from the other, leaving the weighted sum unchanged. Readers may take a valid aggregate as proof that every input was valid, since the doc asks for individual checks only to find which signature failed. The sentence could follow the BIP: a valid aggregate does not prove valid inputs, so inputs that must be valid are verified individually first.
Both points of my earlier comment are fixed. inc_aggregate now reads the input s when n_before is 0, and the generator rebuilds vectors.h byte for byte from bitcoin/bips#2205's vectors at fe78361, incaggregate vector 6 included. Through ctypes, I compared the module with btclib-ecc, an independent implementation: in 300 random cases the two give the same bytes for aggregation and for incremental aggregation from an input s and reject the same input s ≥ n, and in the 267 non-empty ones they accept or reject the same valid and corrupted aggregates. At the previous head the same script fails all 300 cases with an input s. tests, the example and the benchmark pass on macOS arm64. Scripts and output: https://gist.github.com/fametrano/aba13d49caf4e9d0bdbdb3354b7fb100/2aadc3e1529cb27af32c0432323a9d8f09cdbb5a
doc/halfagg.mdsays that aggregating an invalid signature gives an invalid aggregate. That holds for one invalid input, but two can cancel out
Updated the doc to say that a valid aggregate does not imply valid input signatures.
ACK c371cec84c6cfacfe6aace890644b9b6724ae932
I read main_impl.h, tests_impl.h, the header, the example and the benchmark against BIP 458 and its halfagg.py at fe78361, and found no defect in the code. The hash order, the fixed first randomizer, the range checks and the verification equation match the BIP. Lengths and counts are checked before any read or write. I did not read the generator or the data in vectors.h, which regenerates byte for byte.
Only the doc sentence changed since my last review, and it is now correct: invalid inputs crafted to cancel verify in 100 of 100 cases through aggregate and through inc_aggregate, and without the cancelling input they are rejected in 200 of 200. The 300 random cases against btclib-ecc, an independent implementation, still agree. All 256 ctest tests pass on macOS arm64. Under ASan and UBSan, all but the exhaustive test pass, and so do 754 checks of a harness using exact-size heap buffers, including failed calls leaving the output untouched. Of 26 mutations of main_impl.h, the module's tests catch 24. One survivor accepts r ≥ p; I see no feasible signature that would tell it apart. Scripts and output: https://gist.github.com/fametrano/75c48df66cf2b0fa2d666bf0bbf80a02/fb73252aaa096f790fb6a60446aaced7da8cec1c
Not blocking: the other survivor removes aggverify's s range check. test_schnorrsig_aggregate_overflow sets s to all 0xFF, which fails either way, and no vector catches it. aggverify(CTX, NULL, NULL, 0, order, 32) == 0 with s equal to the group order does: I ran it, and it passes now and fails without the check. In test_schnorrsig_aggregate_unforge, the first case passes sizeof(aggsig) as the length, so 1002 of 1024 runs were rejected on length alone; 32*(n+1) would fix that.
Code is based on the secp256k1-zkp implementation. Test vectors based on
BIP 458: https://github.com/bitcoin/bips/blob/master/bip-0458.mediawiki
Co-authored-by: Benedikt <crypto@benedikt-wagner.dev>
In
test_schnorrsig_aggregate_unforge, the first case passessizeof(aggsig)as the length
Addressed by passing 32*(n + 1) as the length now.
the other survivor removes
aggverify's s range check
I don't think this is really needed, but I may add an empty-aggregate vector with s = n to the BIP if I have to touch it again.
reACK c7ed5e0a0dbc69f5f75bc00e516ccd5ee1033e25
The only change since c371cec84c6cfacfe6aace890644b9b6724ae932 is the suggested 32*(n + 1) length in test_schnorrsig_aggregate_unforge. make check passes with the module enabled.