Skip to main content

frost_bluepallas/
negate.rs

1//! Utilities for negating Y coordinates in FROST commitments using the BluePallas curve. This mimics Mina's handling of point negation.
2
3use frost_core::{
4    round1::{Nonce, NonceCommitment, SigningCommitments, SigningNonces},
5    SigningPackage,
6};
7
8use crate::{BluePallas, ChallengeMessage};
9
10/// This trait is used to negate the Y coordinate of the group commitment element with FROST
11/// This is achieved by negating all nonces and commitments produced by all participants
12pub(crate) trait NegateY {
13    /// Negate the Y coordinate of the group element
14    fn negate_y(&self) -> Self;
15}
16
17impl<M> NegateY for SigningNonces<BluePallas<M>>
18where
19    M: ChallengeMessage,
20{
21    fn negate_y(&self) -> Self {
22        let negated_hiding = -self.hiding().to_scalar();
23        let negated_binding = -self.binding().to_scalar();
24        SigningNonces::from_nonces(
25            Nonce::<BluePallas<M>>::from_scalar(negated_hiding),
26            Nonce::<BluePallas<M>>::from_scalar(negated_binding),
27        )
28    }
29}
30
31/// Negate the Y coordinate of the group commitment element with FROST
32impl<M> NegateY for SigningCommitments<BluePallas<M>>
33where
34    M: ChallengeMessage,
35{
36    fn negate_y(&self) -> Self {
37        // Negate the commitments and serialize/deserialize roundtrip
38        let negated_hiding = -self.hiding().value();
39        let negated_binding = -self.binding().value();
40
41        // Create a new SigningCommitments instance with the negated values
42        let negated_hiding_nonce = NoncePallas::<M>::new(negated_hiding);
43        let negated_binding_nonce = NoncePallas::<M>::new(negated_binding);
44
45        SigningCommitments::new(negated_hiding_nonce, negated_binding_nonce)
46    }
47}
48
49type NoncePallas<M> = NonceCommitment<BluePallas<M>>;
50
51/// Take all commitments with a signing package and negate their Y coordinates
52impl<M> NegateY for SigningPackage<BluePallas<M>>
53where
54    M: ChallengeMessage,
55{
56    fn negate_y(&self) -> Self {
57        let negated_commitments = self
58            .signing_commitments()
59            .iter()
60            .map(|(id, commitment)| (*id, commitment.negate_y()))
61            .collect();
62        SigningPackage::new(negated_commitments, self.message())
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use crate::{
69        hasher::hash_to_scalar, BluePallas, ChallengeMessage, PallasGroup, PallasScalarField,
70    };
71
72    use super::*;
73    use alloc::collections::BTreeMap;
74    use ark_ff::UniformRand;
75    use frost_core::{
76        round1::{Nonce, NonceCommitment},
77        Group,
78    };
79    use mina_curves::pasta::ProjectivePallas;
80    use rand_core::OsRng;
81
82    #[derive(Clone, Debug, PartialEq, Eq)]
83    struct TestMessage;
84
85    impl ChallengeMessage for TestMessage {
86        fn challenge(
87            _r: &frost_core::Element<BluePallas<Self>>,
88            _verifying_key: &frost_core::VerifyingKey<BluePallas<Self>>,
89            message: &[u8],
90        ) -> Result<frost_core::Challenge<BluePallas<Self>>, frost_core::Error<BluePallas<Self>>>
91        {
92            Ok(frost_core::Challenge::from_scalar(hash_to_scalar(&[
93                b"test-challenge",
94                message,
95            ])))
96        }
97    }
98
99    type Suite = BluePallas<TestMessage>;
100    type SigningNonces = frost_core::round1::SigningNonces<Suite>;
101    type SigningCommitments = frost_core::round1::SigningCommitments<Suite>;
102    type SigningPackage = frost_core::SigningPackage<Suite>;
103
104    /// Helpers to extract the underlying `PallasGroup` from a `NonceCommitment<BluePallas>`.
105    fn commit_to_group(c: &NonceCommitment<Suite>) -> ProjectivePallas {
106        c.value()
107    }
108
109    #[test]
110    fn signing_nonces_negate_y_inverts_scalars() {
111        let mut rng = OsRng;
112        // make two secret nonces
113        let r1 = <PallasScalarField as frost_core::Field>::Scalar::rand(&mut rng);
114        let r2 = <PallasScalarField as frost_core::Field>::Scalar::rand(&mut rng);
115        let nonces = SigningNonces::from_nonces(
116            Nonce::<Suite>::from_scalar(r1),
117            Nonce::<Suite>::from_scalar(r2),
118        );
119
120        let neg = nonces.negate_y();
121
122        // check hiding nonce
123        let orig_h = nonces.hiding().to_scalar();
124        let new_h = neg.hiding().to_scalar();
125        assert_eq!(
126            orig_h + new_h,
127            <PallasScalarField as frost_core::Field>::zero()
128        );
129
130        // check binding nonce
131        let orig_b = nonces.binding().to_scalar();
132        let new_b = neg.binding().to_scalar();
133        assert_eq!(
134            orig_b + new_b,
135            <PallasScalarField as frost_core::Field>::zero()
136        );
137    }
138
139    #[test]
140    fn signing_commitments_negate_y_inverts_group_elements() {
141        // pick the group generator so we know Y ≠ 0
142        let g = PallasGroup::generator();
143        let g_ser = PallasGroup::serialize(&g).unwrap();
144        let comm = NonceCommitment::<Suite>::deserialize(&g_ser).unwrap();
145
146        let commitments = SigningCommitments::new(comm, comm);
147        let neg = commitments.negate_y();
148
149        // hiding
150        let orig_h = commit_to_group(commitments.hiding());
151        let new_h = commit_to_group(neg.hiding());
152        assert_eq!(new_h, -orig_h);
153
154        // binding
155        let orig_b = commit_to_group(commitments.binding());
156        let new_b = commit_to_group(neg.binding());
157        assert_eq!(new_b, -orig_b);
158    }
159
160    #[test]
161    fn signing_package_negate_y_inverts_all_commitments_and_preserves_message() {
162        // set up one participant (id = 1) with the generator commitment
163        let g = PallasGroup::generator();
164        let g_ser = PallasGroup::serialize(&g).unwrap();
165        let comm = NonceCommitment::<Suite>::deserialize(&g_ser).unwrap();
166        let single = SigningCommitments::new(comm, comm);
167
168        let mut map: BTreeMap<frost_core::Identifier<Suite>, SigningCommitments> = BTreeMap::new();
169        let participant_identifier = frost_core::Identifier::try_from(1u16).unwrap();
170        map.insert(participant_identifier, single);
171        let message = b"hello frost".to_vec();
172
173        let pkg = SigningPackage::new(map.clone(), message.as_slice());
174        let neg_pkg = pkg.negate_y();
175
176        // should preserve the message exactly
177        assert_eq!(neg_pkg.message(), &message[..]);
178
179        // each commitment should be the negation of the original
180        for (id, neg_comm) in neg_pkg.signing_commitments() {
181            let orig = &map[id];
182            let orig_h = commit_to_group(orig.hiding());
183            let new_h = commit_to_group(neg_comm.hiding());
184            assert_eq!(new_h, -orig_h);
185
186            let orig_b = commit_to_group(orig.binding());
187            let new_b = commit_to_group(neg_comm.binding());
188            assert_eq!(new_b, -orig_b);
189        }
190    }
191}