schnorr_fun 0.13.0

BIP340 Schnorr signatures based on secp256kfun
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
use super::{PairedSecretShare, SecretShare, ShareImage, ShareIndex, VerificationShare};
use alloc::vec::Vec;
use core::{marker::PhantomData, ops::Deref};
use secp256kfun::{hash::Hash32, poly, prelude::*};

/// A polynomial where the first coefficient (constant term) is the image of a secret `Scalar` that
/// has been shared in a [Shamir's secret sharing] structure.
///
/// [Shamir's secret sharing]: https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing
#[derive(Clone, Debug, Eq)]
#[cfg_attr(
    feature = "serde",
    derive(crate::fun::serde::Serialize),
    serde(crate = "crate::fun::serde")
)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode))]
pub struct SharedKey<T = Normal, Z = NonZero> {
    /// The public point polynomial that defines the access structure to the FROST key.
    point_polynomial: Vec<Point<Normal, Public, Zero>>,
    #[cfg_attr(feature = "serde", serde(skip))]
    ty: PhantomData<(T, Z)>,
}

impl<T: Normalized, Z: ZeroChoice> SharedKey<T, Z> {
    /// "pair" a secret share that belongs to this shared key so you can keep track of tweaks to the
    /// public key and the secret share together.
    ///
    /// Returns `None` if the secret share is not a valid share of this key.
    pub fn pair_secret_share(&self, secret_share: SecretShare) -> Option<PairedSecretShare<T, Z>> {
        let share_image = poly::point::eval(&self.point_polynomial, secret_share.index);
        if share_image != g!(secret_share.share * G) {
            return None;
        }

        Some(PairedSecretShare::new_unchecked(
            secret_share,
            self.public_key(),
        ))
    }

    /// The threshold number of participants required in a signing coalition to produce a valid signature.
    pub fn threshold(&self) -> usize {
        self.point_polynomial.len()
    }

    /// The internal public polynomial coefficients that defines the public key and the share structure.
    ///
    /// To get the first coefficient of the polynomial typed correctly call [`public_key`].
    ///
    /// [`public_key`]: Self::public_key
    pub fn point_polynomial(&self) -> &[Point<Normal, Public, Zero>] {
        &self.point_polynomial
    }

    /// ☠ Type unsafe: you have to make sure the polynomial fits the type parameters
    fn from_inner(point_polynomial: Vec<Point<Normal, Public, Zero>>) -> Self {
        SharedKey {
            point_polynomial,
            ty: PhantomData,
        }
    }

    /// Converts a `SharedKey` that's was marked as `Zero` to `NonZero`.
    ///
    /// If the shared key *was* actually zero ([`is_zero`] returns true) it returns `None`.
    ///
    /// [`is_zero`]: Self::is_zero
    pub fn non_zero(self) -> Option<SharedKey<Normal, NonZero>> {
        if self.point_polynomial[0].is_zero() {
            return None;
        }

        Some(SharedKey::from_inner(self.point_polynomial))
    }

    /// Whether the shared key is actually zero. i.e. the first coefficient of the sharing polynomial [`is_zero`].
    ///
    /// [`is_zero`]: secp256kfun::Point::is_zero
    pub fn is_zero(&self) -> bool {
        self.point_polynomial[0].is_zero()
    }

    /// Adds a scalar `tweak` to the shared key.
    ///
    /// The returned `SharedKey<Normal, Zero>` represents a sharing of the original value + `tweak`.
    ///
    /// This is useful for deriving unhardened child frost keys from a master frost public key using
    /// [BIP32]. In cases like this since you know that the tweak was computed from a hash of the
    /// original key you call [`non_zero`] and unwrap the `Option` since zero is computationally
    /// unreachable.
    ///
    /// In order for `PairedSecretShare` s to be valid against the new key they will have to apply the same operation.
    ///
    /// If you want to apply an "x-only" tweak you need to call this then [`non_zero`] and finally [`into_xonly`].
    ///
    /// [BIP32]: https://bips.xyz/32
    /// [`non_zero`]: Self::non_zero
    /// [`into_xonly`]: Self::into_xonly
    #[must_use]
    pub fn homomorphic_add(
        mut self,
        tweak: Scalar<impl Secrecy, impl ZeroChoice>,
    ) -> SharedKey<Normal, Zero> {
        self.point_polynomial[0] = g!(self.point_polynomial[0] + tweak * G).normalize();
        SharedKey::from_inner(self.point_polynomial)
    }

    /// Negates the polynomial
    #[must_use]
    pub fn homomorphic_negate(mut self) -> SharedKey<Normal, Z> {
        poly::point::negate(&mut self.point_polynomial);
        SharedKey::from_inner(self.point_polynomial)
    }

    /// Multiplies the shared key by a scalar.
    ///
    /// In order for a [`PairedSecretShare`] to be valid against the new key they will have to apply
    /// [the same operation](super::PairedSecretShare::homomorphic_mul).
    ///
    /// [`PairedSecretShare`]: super::PairedSecretShare
    #[must_use]
    pub fn homomorphic_mul(mut self, tweak: Scalar<impl Secrecy>) -> SharedKey<Normal, Z> {
        for coeff in &mut self.point_polynomial {
            *coeff = g!(tweak * coeff.deref()).normalize();
        }
        SharedKey::from_inner(self.point_polynomial)
    }

    /// The public key that has been shared.
    ///
    /// This is using *public key* in a rather loose sense. Unless it's a `SharedKey<EvenY>` then it
    /// won't be usable as an actual Schnorr [BIP340] public key.
    ///
    /// [BIP340]: https://bips.xyz/340
    pub fn public_key(&self) -> Point<T, Public, Z> {
        // SAFETY: we hold the first coefficient to match the type parameters always
        let public_key = Z::cast_point(self.point_polynomial[0]).expect("invariant");
        T::cast_point(public_key).expect("invariant")
    }

    /// Encodes a `SharedKey` as the compressed encoding of each underlying polynomial coefficient
    ///
    /// i.e. call [`Point::to_bytes`] on each coefficient starting with the constant term. Note that
    /// even if it's a `SharedKey<EvenY>` the first coefficient (A.K.A the public key) will still be
    /// encoded as 33 bytes.
    ///
    /// âš  Unlike other secp256kfun things this doesn't exactly match the serde/bincode
    /// implementations which will length prefix the list of points.
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut bytes = Vec::with_capacity(self.point_polynomial.len() * 33);
        for coeff in &self.point_polynomial {
            bytes.extend(coeff.to_bytes())
        }
        bytes
    }

    /// Decodes a `SharedKey<T,Z>` (for any `T` and `Z`) from a slice.
    ///
    /// Returns `None` if the bytes don't represent points or if the first coefficient doesn't
    /// satisfy the constraints of `T` and `Z`.
    pub fn from_slice(bytes: &[u8]) -> Option<Self> {
        if bytes.is_empty() {
            return None;
        }
        let mut poly = vec![];
        for point_bytes in bytes.chunks(33) {
            poly.push(Point::from_slice(point_bytes)?);
        }

        // check first coefficient satisfies both type parameters
        let first_coeff = Z::cast_point(poly[0])?;
        let _check = T::cast_point(first_coeff)?;

        Some(Self::from_inner(poly))
    }

    /// Gets an image at any index for the shared key. This can't be used for much other than
    /// checking whether `Point` is the correct share image at a certain index. This can be useful
    /// when you load in a share backup and want to check that it's correct.
    ///
    /// To verify a signature share for certain share you should use [`verification_share`] (which
    /// contains a share image).
    ///
    /// [`verification_share`]: Self::verification_share
    pub fn share_image(&self, index: ShareIndex) -> ShareImage {
        ShareImage {
            index,
            image: poly::point::eval(&self.point_polynomial, index).normalize(),
        }
    }

    /// Checks if the polynomial coefficients contain the specified `fingerprint`.
    ///
    /// Verifies that the running hash of coefficients has the required number
    /// of leading zero bits, respecting both per-coefficient and total limits.
    /// This allows detection of shares from the same DKG session and helps
    /// identify corrupted or mismatched shares.
    ///
    /// Returns `Some(n)` where `n` is the number of bits verified if
    /// successful, or `None` if verification failed. Returns `Some(0)` if the
    /// polynomial is too short (≤1 coefficients) to contain a fingerprint. It
    /// will never return more than `max_bits_total` which indicates a complete
    /// match.
    pub fn check_fingerprint<H: crate::fun::hash::Hash32>(
        &self,
        fingerprint: Fingerprint,
    ) -> Option<usize> {
        use crate::fun::hash::HashAdd;

        // the fingerprint is only placed on the non-constant coefficients so it
        // can't be detected with a length 1 polynomial
        if self.point_polynomial.len() <= 1 {
            return Some(0);
        }

        let mut hash_state = H::default()
            .add([fingerprint.tag.len() as u8])
            .add(fingerprint.tag.as_bytes())
            // the public key is unmolested by the fingerprint
            .add(self.point_polynomial[0]);

        let mut verified_bits = 0usize;

        // Check each non-constant coefficient
        for i in 1..self.point_polynomial.len() {
            let remaining_total = fingerprint
                .max_bits_total
                .saturating_sub(verified_bits as u8) as usize;
            if remaining_total == 0 {
                break;
            }

            // Update hash state with next coefficient
            hash_state = hash_state.add(self.point_polynomial[i]);

            let hash_result = hash_state.clone().finalize_fixed();
            let hash_bytes: &[u8] = hash_result.as_ref();

            // NOTE: we don't get the zero bits and just substract it from the
            // total and move on -- we need to make sure each coefficient has at
            // least the required number of bits to make sure it really was part
            // of a fingerprint. Extra bits don't count either.
            let expected_bits = remaining_total.min(fingerprint.bits_per_coeff as usize);
            let actual_bits = Fingerprint::leading_zero_bits(hash_bytes);

            if actual_bits < expected_bits {
                return None;
            }

            verified_bits += expected_bits;
        }

        Some(verified_bits)
    }

    /// Grinds polynomial coefficients to embed the specified `fingerprint` through
    /// proof-of-work.
    ///
    /// For each non-constant coefficient, repeatedly adds the generator point `G`
    /// until the running hash (including the tag, public key, and all previous
    /// coefficients) has at least `fingerprint.bit_length` leading zero bits.
    /// This process modifies the polynomial while preserving the shared secret.
    ///
    /// Returns a scalar polynomial where the constant term is always zero
    /// and the remaining coefficients indicate how many times `G` was added
    /// to each polynomial coefficient. This polynomial can be added to secret
    /// shares using [`SecretShare::homomorphic_poly_add`] to maintain consistency.
    ///
    /// The computational cost increases exponentially with `bit_length`: each
    /// additional bit doubles the expected work required.
    pub fn grind_fingerprint<H: Hash32>(
        &mut self,
        fingerprint: Fingerprint,
    ) -> Vec<Scalar<Public, Zero>> {
        let mut tweaks = Vec::with_capacity(self.threshold());
        // We don't mutate the first coefficient
        tweaks.push(Scalar::<Public, _>::zero());

        if self.point_polynomial.len() <= 1 {
            // only mutate non-constant terms
            return tweaks;
        }

        use secp256kfun::hash::HashAdd;
        let mut hash_state = H::default()
            .add([fingerprint.tag.len() as u8])
            .add(fingerprint.tag.as_bytes())
            // the public key is unmolested from the fingerprint grinding
            .add(self.point_polynomial[0]);

        let mut grinded_bits = 0usize;

        for coeff in &mut self.point_polynomial[1..] {
            let mut total_tweak = Scalar::<Public, Zero>::zero();
            let mut current_coeff = *coeff;
            let remaining_total = fingerprint
                .max_bits_total
                .saturating_sub(grinded_bits as u8) as usize;

            if remaining_total == 0 {
                break;
            }

            // Each coefficient should have at most max_bits_per_coeff bits
            let needed_bits = remaining_total.min(fingerprint.bits_per_coeff as usize);

            loop {
                // Clone the hash state from previous coefficients and add current one
                let hash = hash_state.clone().add(current_coeff);
                // Check if hash has required number of leading zero bits
                let hash_bytes: [u8; 32] = hash.clone().finalize_fixed().into();
                if Fingerprint::leading_zero_bits(&hash_bytes[..]) >= needed_bits {
                    // Update hash_state for next coefficient because the next coefficient hash
                    // includes the current one.
                    hash_state = hash;
                    grinded_bits += needed_bits;
                    break;
                }

                // Add one more G to the coefficient
                total_tweak += s!(1);
                current_coeff = g!(current_coeff + G).normalize();
            }

            // Apply the final tweak to the polynomial coefficient
            *coeff = current_coeff;
            tweaks.push(total_tweak);
        }

        tweaks
    }
}

impl SharedKey {
    /// Convert the key into a [BIP340] "x-only" SharedKey.
    ///
    /// This is the [BIP340] compatible version of the key which you can put in a segwitv1 output.
    ///
    /// [BIP340]: https://bips.xyz/340
    pub fn into_xonly(mut self) -> SharedKey<EvenY> {
        let needs_negation = !self.public_key().is_y_even();
        if needs_negation {
            self = self.homomorphic_negate();
            debug_assert!(self.public_key().is_y_even());
        }

        SharedKey::from_inner(self.point_polynomial)
    }

    /// Creates a non-zero `SharedKey` from a known `NonZero` first coefficient the other coefficients in `rest`.
    pub fn from_non_zero_poly<Z>(
        first_coef: Point,
        rest: impl IntoIterator<Item = Point<Normal, Public, Z>>,
    ) -> Self {
        let mut poly = vec![first_coef.mark_zero()];
        for point in rest.into_iter() {
            poly.push(point.mark_zero());
        }
        Self::from_inner(poly)
    }
}

impl SharedKey<Normal, Zero> {
    /// Constructor to create a shared key from a vector of points where each item represent a polynomial
    /// coefficient.
    ///
    /// The resulting shared key will be `SharedKey<Normal, Zero>`. It's up to the caller to do the zero check with [`non_zero`]
    ///
    /// [`non_zero`]: Self::non_zero
    pub fn from_poly(poly: Vec<Point<Normal, Public, Zero>>) -> Self {
        if poly.is_empty() {
            // an empty polynomial is represented as a vector with a single zero item to avoid
            // panics
            return Self::from_poly(vec![Point::zero()]);
        }

        SharedKey::from_inner(poly)
    }

    /// Create a shared key from a subset of share images.
    ///
    /// If all the share images are correct and you have at least a threshold of them then you'll
    /// get the original shared key. If you put in a wrong share you won't get the right answer and
    /// there will be **no error**.
    ///
    /// Note that a "share image" is not a concept that we really use in the core of this library
    /// but you can get one from a share with [`SecretShare::share_image`].
    ///
    /// ## Security
    ///
    /// âš  You can't just take any points you want and pass them in here and hope it's secure.
    /// They need to be from a securely generated key.
    pub fn from_share_images(share_images: impl IntoIterator<Item = ShareImage>) -> Self {
        let shares: Vec<(ShareIndex, Point<Normal, Public, Zero>)> = share_images
            .into_iter()
            .map(|img| (img.index, img.image))
            .collect();
        let poly = poly::point::interpolate(&shares);
        let poly = poly::point::normalize(poly);
        SharedKey::from_inner(poly.collect())
    }
}

impl SharedKey<EvenY> {
    /// Creates a verification share for a party at the given index.
    ///
    /// The verification share is the image of the party's secret share evaluated from this key's polynomial.
    ///
    /// **Important**: The returned `VerificationShare` can only be used to verify signatures
    /// against the specific `SharedKey` that created it. You must ensure you're verifying
    /// against the correct shared key. If the polynomial has been tweaked or modified,
    /// you'll need to get a new verification share from the modified key.
    pub fn verification_share(&self, index: ShareIndex) -> VerificationShare {
        let image = poly::point::eval(&self.point_polynomial, index);
        VerificationShare(ShareImage { index, image })
    }
}

impl<T1, Z1, T2, Z2> PartialEq<SharedKey<T2, Z2>> for SharedKey<T1, Z1> {
    fn eq(&self, other: &SharedKey<T2, Z2>) -> bool {
        other.point_polynomial == self.point_polynomial
    }
}

#[cfg(feature = "bincode")]
impl<Context, T: PointType, Z: ZeroChoice> bincode::Decode<Context> for SharedKey<T, Z> {
    fn decode<D: secp256kfun::bincode::de::Decoder>(
        decoder: &mut D,
    ) -> Result<Self, secp256kfun::bincode::error::DecodeError> {
        use secp256kfun::bincode::error::DecodeError;
        let poly = Vec::<Point<Normal, Public, Zero>>::decode(decoder)?;
        let first = poly
            .first()
            .copied()
            .ok_or(DecodeError::Other("empty polynomial for shared key"))?;
        let first_coeff = Z::cast_point(first).ok_or(DecodeError::Other(
            "zero public key for non-zero shared key",
        ))?;
        let _check = T::cast_point(first_coeff)
            .ok_or(DecodeError::Other("odd-y public key for even-y shared key"))?;

        Ok(SharedKey {
            point_polynomial: poly,
            ty: PhantomData,
        })
    }
}

#[cfg(feature = "serde")]
impl<'de, T: PointType, Z: ZeroChoice> crate::fun::serde::Deserialize<'de> for SharedKey<T, Z> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: secp256kfun::serde::Deserializer<'de>,
    {
        let poly = Vec::<Point<Normal, Public, Zero>>::deserialize(deserializer)?;

        let first = poly
            .first()
            .copied()
            .ok_or(crate::fun::serde::de::Error::custom(
                "empty polynomial for shared key",
            ))?;
        let first_coeff = Z::cast_point(first).ok_or(crate::fun::serde::de::Error::custom(
            "zero public key for non-zero shared key",
        ))?;

        let _check = T::cast_point(first_coeff).ok_or(crate::fun::serde::de::Error::custom(
            "odd-y public key for even-y shared key",
        ))?;

        Ok(Self {
            point_polynomial: poly,
            ty: PhantomData,
        })
    }
}

#[cfg(feature = "bincode")]
bincode::impl_borrow_decode!(SharedKey<Normal, Zero>);
#[cfg(feature = "bincode")]
bincode::impl_borrow_decode!(SharedKey<Normal, NonZero>);
#[cfg(feature = "bincode")]
bincode::impl_borrow_decode!(SharedKey<EvenY, NonZero>);

/// Configuration for polynomial fingerprinting in DKG protocols.
///
/// A `Fingerprint` allows coordinators to embed proof-of-work into polynomial
/// coefficients during distributed key generation. This helps detect when shares
/// from different DKG sessions are accidentally mixed and provides resistance
/// against resource exhaustion attacks.
///
/// The fingerprint is computed by hashing the public key with each subsequent
/// coefficient, requiring each hash to have a minimum number of leading zero bits.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Fingerprint {
    /// Number of leading zero bits required in the hash
    pub bits_per_coeff: u8,
    /// Domain separator tag to include in the hash
    pub tag: &'static str,
    /// Max number of bits for the total
    pub max_bits_total: u8,
}

impl Fingerprint {
    /// The default fingerprint used for share generation in production
    pub const FROST_V0: Self = Self {
        bits_per_coeff: 18,
        max_bits_total: 18 * 2,
        tag: "frost-v0",
    };

    /// a 0-bit fingerprint which means it will have no affect.
    pub const NONE: Self = Self {
        bits_per_coeff: 0,
        tag: "",
        max_bits_total: 0,
    };

    /// Count leading zero bits across a byte slice
    pub(crate) fn leading_zero_bits(bytes: &[u8]) -> usize {
        let mut count = 0;
        for &b in bytes {
            if b == 0 {
                count += 8;
            } else {
                count += b.leading_zeros() as usize;
                break;
            }
        }
        count
    }
}

impl Default for Fingerprint {
    fn default() -> Self {
        Self::FROST_V0
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn fingerprint_grinding_respects_max_bits_total() {
        // Create a polynomial with 4 coefficients (1 constant + 3 non-constant)
        // This gives us a threshold-3 scheme
        let mut shared_key = SharedKey::<Normal, Zero>::from_poly(
            poly::point::normalize(vec![
                g!(1 * G).mark_zero(), // Public key (constant term)
                g!(2 * G).mark_zero(), // First non-constant
                g!(3 * G).mark_zero(), // Second non-constant
                g!(4 * G).mark_zero(), // Third non-constant
            ])
            .collect(),
        );

        let original_coeffs = shared_key.point_polynomial.clone();

        let fingerprint = Fingerprint {
            bits_per_coeff: 5,
            max_bits_total: 10,
            tag: "test",
        };

        // Grind the fingerprint
        let tweaks = shared_key.grind_fingerprint::<sha2::Sha256>(fingerprint);

        // Check that we got 3 tweaks (one for constant + two for the grinded non-constant coefficients)
        // The function stops early when max_bits_total is reached, so the vector is shorter
        // Missing elements implicitly mean zero tweaks
        assert_eq!(
            tweaks.len(),
            3,
            "Should only have tweaks for coefficients that were processed"
        );

        // First tweak should be zero (constant coefficient is not mutated)
        assert_eq!(tweaks[0], Scalar::<Public, Zero>::zero());

        // First two non-constant coefficients should be mutated (have non-zero tweaks)
        assert_ne!(
            tweaks[1],
            Scalar::<Public, Zero>::zero(),
            "First non-constant coefficient should be mutated"
        );
        assert_ne!(
            tweaks[2],
            Scalar::<Public, Zero>::zero(),
            "Second non-constant coefficient should be mutated"
        );

        // The absence of tweaks[3] means the third coefficient wasn't mutated (implicitly zero)

        // Verify the polynomial was actually modified
        assert_eq!(
            shared_key.point_polynomial[0], original_coeffs[0],
            "Constant coefficient should be unchanged"
        );
        assert_ne!(
            shared_key.point_polynomial[1], original_coeffs[1],
            "First coefficient should be changed"
        );
        assert_ne!(
            shared_key.point_polynomial[2], original_coeffs[2],
            "Second coefficient should be changed"
        );
        assert_eq!(
            shared_key.point_polynomial[3], original_coeffs[3],
            "Third coefficient should be unchanged"
        );

        // Verify the fingerprint is valid
        assert!(
            shared_key
                .check_fingerprint::<sha2::Sha256>(fingerprint)
                .is_some(),
            "Grinded fingerprint should be valid"
        );
    }

    #[cfg(feature = "bincode")]
    #[test]
    fn bincode_encoding_decoding_roundtrip() {
        let poly_zero = SharedKey::<Normal, Zero>::from_poly(
            poly::point::normalize(vec![
                g!(0 * G),
                g!(1 * G).mark_zero(),
                g!(2 * G).mark_zero(),
            ])
            .collect(),
        );
        let poly_one = SharedKey::<Normal, Zero>::from_poly(
            poly::point::normalize(vec![
                g!(1 * G).mark_zero(),
                g!(2 * G).mark_zero(),
                g!(3 * G).mark_zero(),
            ])
            .collect(),
        )
        .non_zero()
        .unwrap()
        .into_xonly();

        let poly_minus_one = SharedKey::<Normal, Zero>::from_poly(
            poly::point::normalize(vec![
                g!(-1 * G).mark_zero(),
                g!(2 * G).mark_zero(),
                g!(3 * G).mark_zero(),
            ])
            .collect(),
        )
        .non_zero()
        .unwrap();

        let bytes_poly_zero =
            bincode::encode_to_vec(&poly_zero, bincode::config::standard()).unwrap();
        let bytes_poly_one =
            bincode::encode_to_vec(&poly_one, bincode::config::standard()).unwrap();
        let bytes_poly_minus_one =
            bincode::encode_to_vec(&poly_minus_one, bincode::config::standard()).unwrap();

        let (poly_zero_got, _) = bincode::decode_from_slice::<SharedKey<Normal, Zero>, _>(
            &bytes_poly_zero,
            bincode::config::standard(),
        )
        .unwrap();
        let (poly_one_got, _) = bincode::decode_from_slice::<SharedKey<EvenY, NonZero>, _>(
            &bytes_poly_one,
            bincode::config::standard(),
        )
        .unwrap();

        let (poly_minus_one_got, _) = bincode::decode_from_slice::<SharedKey<Normal, NonZero>, _>(
            &bytes_poly_minus_one,
            bincode::config::standard(),
        )
        .unwrap();

        assert!(
            bincode::decode_from_slice::<SharedKey<Normal, NonZero>, _>(
                &bytes_poly_zero,
                bincode::config::standard(),
            )
            .is_err()
        );

        assert!(
            bincode::decode_from_slice::<SharedKey<EvenY, NonZero>, _>(
                &bytes_poly_minus_one,
                bincode::config::standard(),
            )
            .is_err()
        );

        assert_eq!(poly_zero_got, poly_zero);
        assert_eq!(poly_one_got, poly_one);
        assert_eq!(poly_minus_one_got, poly_minus_one);
    }

    #[test]
    fn to_bytes_from_slice_roudtrip() {
        let poly_zero = SharedKey::<Normal, Zero>::from_poly(
            poly::point::normalize(vec![
                g!(0 * G),
                g!(1 * G).mark_zero(),
                g!(2 * G).mark_zero(),
            ])
            .collect(),
        );
        let poly_one = SharedKey::<Normal, Zero>::from_poly(
            poly::point::normalize(vec![
                g!(1 * G).mark_zero(),
                g!(2 * G).mark_zero(),
                g!(3 * G).mark_zero(),
            ])
            .collect(),
        )
        .non_zero()
        .unwrap()
        .into_xonly();

        let poly_minus_one = SharedKey::<Normal, Zero>::from_poly(
            poly::point::normalize(vec![
                g!(-1 * G).mark_zero(),
                g!(2 * G).mark_zero(),
                g!(3 * G).mark_zero(),
            ])
            .collect(),
        )
        .non_zero()
        .unwrap();

        let bytes_poly_zero = poly_zero.to_bytes();
        let bytes_poly_one = poly_one.to_bytes();
        let bytes_poly_minus_one = poly_minus_one.to_bytes();

        let poly_zero_got = SharedKey::<Normal, Zero>::from_slice(&bytes_poly_zero[..]).unwrap();
        let poly_one_got = SharedKey::<EvenY, NonZero>::from_slice(&bytes_poly_one).unwrap();
        let poly_minus_one_got =
            SharedKey::<Normal, NonZero>::from_slice(&bytes_poly_minus_one[..]).unwrap();

        assert!(SharedKey::<Normal, NonZero>::from_slice(&bytes_poly_zero[..]).is_none());
        assert!(SharedKey::<EvenY, NonZero>::from_slice(&bytes_poly_minus_one[..]).is_none());

        assert_eq!(poly_zero_got, poly_zero);
        assert_eq!(poly_one_got, poly_one);
        assert_eq!(poly_minus_one_got, poly_minus_one);
    }
}