penumbra_sdk_custody/threshold/
sign.rs

1use std::{
2    collections::{BTreeMap, HashMap},
3    iter,
4};
5
6use anyhow::{anyhow, Result};
7use ed25519_consensus::{Signature, SigningKey, VerificationKey};
8use penumbra_sdk_keys::FullViewingKey;
9use rand_core::CryptoRngCore;
10
11use decaf377_frost as frost;
12use frost::round1::SigningCommitments;
13use penumbra_sdk_proto::core::component::{
14    governance::v1::ValidatorVoteBody as ProtoValidatorVoteBody,
15    stake::v1::Validator as ProtoValidator,
16};
17use penumbra_sdk_proto::{penumbra::custody::threshold::v1 as pb, DomainType, Message};
18use penumbra_sdk_transaction::AuthorizationData;
19use penumbra_sdk_txhash::EffectHash;
20
21use crate::terminal::SigningRequest;
22
23use super::{config::Config, SigningResponse};
24
25/// Represents the message sent by the coordinator at the start of the signing process.
26///
27/// This is nominally "round 1", even though it's the only message the coordinator ever sends.
28#[derive(Debug, Clone)]
29pub struct CoordinatorRound1 {
30    request: SigningRequest,
31}
32
33impl CoordinatorRound1 {
34    /// View the transaction plan associated with the first message.
35    ///
36    /// We need this method to be able to prompt users correctly.
37    pub fn signing_request(&self) -> &SigningRequest {
38        &self.request
39    }
40}
41
42impl From<CoordinatorRound1> for pb::CoordinatorRound1 {
43    fn from(value: CoordinatorRound1) -> Self {
44        match value.request {
45            SigningRequest::TransactionPlan(plan) => Self {
46                request: Some(pb::coordinator_round1::Request::Plan(plan.into())),
47            },
48            SigningRequest::ValidatorDefinition(validator) => Self {
49                request: Some(pb::coordinator_round1::Request::ValidatorDefinition(
50                    ProtoValidator::from(validator).into(),
51                )),
52            },
53            SigningRequest::ValidatorVote(vote) => Self {
54                request: Some(pb::coordinator_round1::Request::ValidatorVote(
55                    ProtoValidatorVoteBody::from(vote).into(),
56                )),
57            },
58        }
59    }
60}
61
62impl TryFrom<pb::CoordinatorRound1> for CoordinatorRound1 {
63    type Error = anyhow::Error;
64
65    fn try_from(value: pb::CoordinatorRound1) -> Result<Self, Self::Error> {
66        match value
67            .request
68            .ok_or_else(|| anyhow::anyhow!("missing request"))?
69        {
70            pb::coordinator_round1::Request::Plan(plan) => Ok(Self {
71                request: SigningRequest::TransactionPlan(plan.try_into()?),
72            }),
73            pb::coordinator_round1::Request::ValidatorDefinition(def) => Ok(Self {
74                request: SigningRequest::ValidatorDefinition(def.try_into()?),
75            }),
76            pb::coordinator_round1::Request::ValidatorVote(vote) => Ok(Self {
77                request: SigningRequest::ValidatorVote(vote.try_into()?),
78            }),
79        }
80    }
81}
82
83impl DomainType for CoordinatorRound1 {
84    type Proto = pb::CoordinatorRound1;
85}
86
87#[derive(Debug, Clone)]
88pub struct CoordinatorRound2 {
89    // For each thing to sign, a map from FROST identifiers to a pair of commitments.
90    all_commitments: Vec<BTreeMap<frost::Identifier, frost::round1::SigningCommitments>>,
91}
92
93fn commitments_to_pb(
94    commitments: impl IntoIterator<Item = frost::round1::SigningCommitments>,
95) -> pb::follower_round1::Inner {
96    pb::follower_round1::Inner {
97        commitments: commitments.into_iter().map(|x| x.into()).collect(),
98    }
99}
100
101impl From<CoordinatorRound2> for pb::CoordinatorRound2 {
102    fn from(value: CoordinatorRound2) -> Self {
103        Self {
104            signing_packages: value
105                .all_commitments
106                .into_iter()
107                .map(|x| pb::coordinator_round2::PartialSigningPackage {
108                    all_commitments: x
109                        .into_iter()
110                        .map(
111                            |(id, commitment)| pb::coordinator_round2::IdentifiedCommitments {
112                                identifier: id.serialize(),
113                                commitments: Some(commitment.into()),
114                            },
115                        )
116                        .collect(),
117                })
118                .collect(),
119        }
120    }
121}
122
123impl TryFrom<pb::CoordinatorRound2> for CoordinatorRound2 {
124    type Error = anyhow::Error;
125
126    fn try_from(value: pb::CoordinatorRound2) -> std::result::Result<Self, Self::Error> {
127        Ok(Self {
128            all_commitments: value
129                .signing_packages
130                .into_iter()
131                .map(|x| {
132                    let mut acc = BTreeMap::new();
133                    for id_commitment in x.all_commitments {
134                        let identifier = frost::Identifier::deserialize(&id_commitment.identifier)?;
135                        if acc.contains_key(&identifier) {
136                            anyhow::bail!(
137                                "duplicate key when deserializing CoordinatorRound2: {:?}",
138                                &identifier
139                            );
140                        }
141                        let commitment = id_commitment
142                            .commitments
143                            .ok_or(anyhow!("CoordinatorRound2 missing commitments"))?
144                            .try_into()?;
145                        acc.insert(identifier, commitment);
146                    }
147                    Ok(acc)
148                })
149                .collect::<Result<Vec<_>, _>>()?,
150        })
151    }
152}
153
154impl DomainType for CoordinatorRound2 {
155    type Proto = pb::CoordinatorRound2;
156}
157
158/// The message sent by the followers in round1 of signing.
159#[derive(Debug, Clone)]
160pub struct FollowerRound1 {
161    /// A commitment for each spend we need to authorize.
162    pub(self) commitments: Vec<frost::round1::SigningCommitments>,
163    /// A verification key identifying who the sender is.
164    pub(self) pk: VerificationKey,
165    /// The signature over the protobuf encoding of the commitments.
166    pub(self) sig: Signature,
167}
168
169impl From<FollowerRound1> for pb::FollowerRound1 {
170    fn from(value: FollowerRound1) -> Self {
171        Self {
172            inner: Some(commitments_to_pb(value.commitments)),
173            pk: Some(pb::VerificationKey {
174                inner: value.pk.to_bytes().to_vec(),
175            }),
176            sig: Some(pb::Signature {
177                inner: value.sig.to_bytes().to_vec(),
178            }),
179        }
180    }
181}
182
183impl TryFrom<pb::FollowerRound1> for FollowerRound1 {
184    type Error = anyhow::Error;
185
186    fn try_from(value: pb::FollowerRound1) -> Result<Self, Self::Error> {
187        Ok(Self {
188            commitments: value
189                .inner
190                .ok_or(anyhow!("missing inner"))?
191                .commitments
192                .into_iter()
193                .map(|x| x.try_into())
194                .collect::<Result<Vec<_>, _>>()?,
195            pk: value
196                .pk
197                .ok_or(anyhow!("missing pk"))?
198                .inner
199                .as_slice()
200                .try_into()?,
201            sig: value
202                .sig
203                .ok_or(anyhow!("missing sig"))?
204                .inner
205                .as_slice()
206                .try_into()?,
207        })
208    }
209}
210
211impl FollowerRound1 {
212    // Make a round1 message, automatically signing the right bytes
213    fn make(signing_key: &SigningKey, commitments: Vec<SigningCommitments>) -> Self {
214        Self {
215            commitments: commitments.clone(),
216            pk: signing_key.verification_key(),
217            sig: signing_key.sign(&commitments_to_pb(commitments).encode_to_vec()),
218        }
219    }
220
221    // Extract the commitments from this struct, checking the signature
222    fn checked_commitments(self) -> Result<(VerificationKey, Vec<SigningCommitments>)> {
223        self.pk.verify(
224            &self.sig,
225            &commitments_to_pb(self.commitments.clone()).encode_to_vec(),
226        )?;
227        Ok((self.pk, self.commitments))
228    }
229}
230
231impl DomainType for FollowerRound1 {
232    type Proto = pb::FollowerRound1;
233}
234
235fn shares_to_pb(shares: Vec<frost::round2::SignatureShare>) -> pb::follower_round2::Inner {
236    pb::follower_round2::Inner {
237        shares: shares.into_iter().map(|x| x.into()).collect(),
238    }
239}
240
241/// The message sent by the followers in round2 of signing.
242#[derive(Debug, Clone)]
243pub struct FollowerRound2 {
244    /// A share of each signature we need to produce.
245    pub(self) shares: Vec<frost::round2::SignatureShare>,
246    /// A verification key identifying who the sender is.
247    pub(self) pk: VerificationKey,
248    /// The signature over the protobuf encoding of the sahres.
249    pub(self) sig: Signature,
250}
251
252impl From<FollowerRound2> for pb::FollowerRound2 {
253    fn from(value: FollowerRound2) -> Self {
254        Self {
255            inner: Some(shares_to_pb(value.shares)),
256            pk: Some(pb::VerificationKey {
257                inner: value.pk.to_bytes().to_vec(),
258            }),
259            sig: Some(pb::Signature {
260                inner: value.sig.to_bytes().to_vec(),
261            }),
262        }
263    }
264}
265
266impl TryFrom<pb::FollowerRound2> for FollowerRound2 {
267    type Error = anyhow::Error;
268
269    fn try_from(value: pb::FollowerRound2) -> Result<Self, Self::Error> {
270        Ok(Self {
271            shares: value
272                .inner
273                .ok_or(anyhow!("missing inner"))?
274                .shares
275                .into_iter()
276                .map(|x| x.try_into())
277                .collect::<Result<Vec<_>, _>>()?,
278            pk: value
279                .pk
280                .ok_or(anyhow!("missing pk"))?
281                .inner
282                .as_slice()
283                .try_into()?,
284            sig: value
285                .sig
286                .ok_or(anyhow!("missing sig"))?
287                .inner
288                .as_slice()
289                .try_into()?,
290        })
291    }
292}
293
294impl FollowerRound2 {
295    // Make a round1 message, automatically signing the right bytes
296    fn make(signing_key: &SigningKey, shares: Vec<frost::round2::SignatureShare>) -> Self {
297        Self {
298            shares: shares.clone(),
299            pk: signing_key.verification_key(),
300            sig: signing_key.sign(&shares_to_pb(shares).encode_to_vec()),
301        }
302    }
303
304    // Extract the commitments from this struct, checking the signature
305    fn checked_shares(self) -> Result<(VerificationKey, Vec<frost::round2::SignatureShare>)> {
306        self.pk.verify(
307            &self.sig,
308            &shares_to_pb(self.shares.clone()).encode_to_vec(),
309        )?;
310        Ok((self.pk, self.shares))
311    }
312}
313
314impl DomainType for FollowerRound2 {
315    type Proto = pb::FollowerRound2;
316}
317
318/// Calculate the number of required signatures for a plan.
319///
320/// A plan can require more than one signature, hence the need for this method.
321fn required_signatures(request: &SigningRequest) -> usize {
322    match request {
323        SigningRequest::TransactionPlan(plan) => {
324            plan.spend_plans().count()
325                + plan.delegator_vote_plans().count()
326                + plan.lqt_vote_plans().count()
327        }
328        SigningRequest::ValidatorDefinition(_) => 1,
329        SigningRequest::ValidatorVote(_) => 1,
330    }
331}
332
333/// Create a trivial signing response if no signatures are needed.
334pub fn no_signature_response(
335    fvk: &FullViewingKey,
336    request: &SigningRequest,
337) -> Result<Option<SigningResponse>> {
338    match request {
339        SigningRequest::TransactionPlan(plan) if required_signatures(request) == 0 => {
340            Ok(Some(SigningResponse::Transaction(AuthorizationData {
341                effect_hash: Some(plan.effect_hash(fvk)?),
342                spend_auths: Vec::new(),
343                delegator_vote_auths: Vec::new(),
344                lqt_vote_auths: Vec::new(),
345            })))
346        }
347        _ => Ok(None),
348    }
349}
350
351pub struct CoordinatorState1 {
352    request: SigningRequest,
353    my_round1_reply: FollowerRound1,
354    my_round1_state: FollowerState,
355}
356
357pub struct CoordinatorState2 {
358    request: SigningRequest,
359    my_round2_reply: FollowerRound2,
360    to_be_signed: ToBeSigned,
361    signing_packages: Vec<frost::SigningPackage>,
362}
363
364enum ToBeSigned {
365    EffectHash(EffectHash),
366    ValidatorDefinitionBytes(Vec<u8>),
367    ValidatorVoteBytes(Vec<u8>),
368}
369
370impl SigningRequest {
371    fn to_be_signed(&self, config: &Config) -> Result<ToBeSigned> {
372        let out = match self {
373            SigningRequest::TransactionPlan(plan) => {
374                ToBeSigned::EffectHash(plan.effect_hash(config.fvk())?)
375            }
376            SigningRequest::ValidatorDefinition(validator) => ToBeSigned::ValidatorDefinitionBytes(
377                ProtoValidator::from(validator.clone()).encode_to_vec(),
378            ),
379            SigningRequest::ValidatorVote(vote) => ToBeSigned::ValidatorVoteBytes(
380                ProtoValidatorVoteBody::from(vote.clone()).encode_to_vec(),
381            ),
382        };
383        Ok(out)
384    }
385}
386
387impl AsRef<[u8]> for ToBeSigned {
388    fn as_ref(&self) -> &[u8] {
389        match self {
390            ToBeSigned::EffectHash(x) => x.as_ref(),
391            ToBeSigned::ValidatorDefinitionBytes(x) => x.as_slice(),
392            ToBeSigned::ValidatorVoteBytes(x) => x.as_slice(),
393        }
394    }
395}
396
397pub struct FollowerState {
398    request: SigningRequest,
399    nonces: Vec<frost::round1::SigningNonces>,
400}
401
402pub fn coordinator_round1(
403    rng: &mut impl CryptoRngCore,
404    config: &Config,
405    request: SigningRequest,
406) -> Result<(CoordinatorRound1, CoordinatorState1)> {
407    let message = CoordinatorRound1 {
408        request: request.clone(),
409    };
410    let (my_round1_reply, my_round1_state) = follower_round1(rng, config, message.clone())?;
411    let state = CoordinatorState1 {
412        request,
413        my_round1_reply,
414        my_round1_state,
415    };
416    Ok((message, state))
417}
418
419pub fn coordinator_round2(
420    config: &Config,
421    state: CoordinatorState1,
422    follower_messages: &[FollowerRound1],
423) -> Result<(CoordinatorRound2, CoordinatorState2)> {
424    let mut all_commitments = vec![BTreeMap::new(); required_signatures(&state.request)];
425    for message in follower_messages
426        .iter()
427        .cloned()
428        .chain(iter::once(state.my_round1_reply))
429    {
430        let (pk, commitments) = message.checked_commitments()?;
431        if !config.verification_keys().contains(&pk) {
432            anyhow::bail!("unknown verification key: {:?}", pk);
433        }
434        // The public key acts as the identifier
435        let identifier = frost::Identifier::derive(pk.as_bytes().as_slice())?;
436        for (tree_i, com_i) in all_commitments.iter_mut().zip(commitments.into_iter()) {
437            tree_i.insert(identifier, com_i);
438        }
439    }
440    let reply = CoordinatorRound2 { all_commitments };
441
442    let my_round2_reply = follower_round2(config, state.my_round1_state, reply.clone())?;
443
444    let to_be_signed = state.request.to_be_signed(&config)?;
445
446    let signing_packages = {
447        reply
448            .all_commitments
449            .iter()
450            .map(|tree| frost::SigningPackage::new(tree.clone(), to_be_signed.as_ref()))
451            .collect()
452    };
453    let state = CoordinatorState2 {
454        request: state.request,
455        my_round2_reply,
456        to_be_signed,
457        signing_packages,
458    };
459    Ok((reply, state))
460}
461
462pub fn coordinator_round3(
463    config: &Config,
464    state: CoordinatorState2,
465    follower_messages: &[FollowerRound2],
466) -> Result<SigningResponse> {
467    let mut share_maps: Vec<HashMap<frost::Identifier, frost::round2::SignatureShare>> =
468        vec![HashMap::new(); required_signatures(&state.request)];
469    for message in follower_messages
470        .iter()
471        .cloned()
472        .chain(iter::once(state.my_round2_reply))
473    {
474        let (pk, shares) = message.checked_shares()?;
475        if !config.verification_keys().contains(&pk) {
476            anyhow::bail!("unknown verification key: {:?}", pk);
477        }
478        let identifier = frost::Identifier::derive(pk.as_bytes().as_slice())?;
479        for (map_i, share_i) in share_maps.iter_mut().zip(shares.into_iter()) {
480            map_i.insert(identifier, share_i);
481        }
482    }
483
484    match state.request {
485        SigningRequest::TransactionPlan(plan) => {
486            let mut spend_auths = plan
487                .spend_plans()
488                .map(|x| x.randomizer)
489                .chain(plan.delegator_vote_plans().map(|x| x.randomizer))
490                .chain(plan.lqt_vote_plans().map(|x| x.randomizer))
491                .zip(share_maps.iter())
492                .zip(state.signing_packages.iter())
493                .map(|((randomizer, share_map), signing_package)| {
494                    frost::aggregate_randomized(
495                        signing_package,
496                        &share_map,
497                        &config.public_key_package(),
498                        randomizer,
499                    )
500                })
501                .collect::<Result<Vec<_>, _>>()?;
502            let num_spend_auths = plan.spend_plans().count();
503            let num_delegator_auths = plan.delegator_vote_plans().count();
504
505            let lqt_vote_auths = spend_auths.split_off(num_spend_auths + num_delegator_auths);
506            let delegator_vote_auths = spend_auths.split_off(num_spend_auths);
507            Ok(SigningResponse::Transaction(AuthorizationData {
508                effect_hash: {
509                    let ToBeSigned::EffectHash(effect_hash) = state.to_be_signed else {
510                        unreachable!("transaction plan request has non-effect-hash to be signed");
511                    };
512                    Some(effect_hash)
513                },
514                spend_auths,
515                delegator_vote_auths,
516                lqt_vote_auths,
517            }))
518        }
519        SigningRequest::ValidatorDefinition(_) => {
520            let validator_definition_auth = share_maps
521                .get(0)
522                .ok_or_else(|| anyhow!("missing signature for validator definition"))?;
523            Ok(SigningResponse::ValidatorDefinition(frost::aggregate(
524                &state
525                    .signing_packages
526                    .get(0)
527                    .expect("same number of signing packages as signatures"),
528                &validator_definition_auth,
529                &config.public_key_package(),
530            )?))
531        }
532        SigningRequest::ValidatorVote(_) => {
533            let validator_vote_auth = share_maps
534                .get(0)
535                .ok_or_else(|| anyhow!("missing signature for validator vote"))?;
536            Ok(SigningResponse::ValidatorVote(frost::aggregate(
537                &state
538                    .signing_packages
539                    .get(0)
540                    .expect("same number of signing packages as signatures"),
541                &validator_vote_auth,
542                &config.public_key_package(),
543            )?))
544        }
545    }
546}
547
548pub fn follower_round1(
549    rng: &mut impl CryptoRngCore,
550    config: &Config,
551    coordinator: CoordinatorRound1,
552) -> Result<(FollowerRound1, FollowerState)> {
553    let required = required_signatures(&coordinator.request);
554    let (nonces, commitments) = (0..required)
555        .map(|_| frost::round1::commit(&config.key_package().secret_share(), rng))
556        .unzip();
557    let reply = FollowerRound1::make(config.signing_key(), commitments);
558    let state = FollowerState {
559        request: coordinator.request,
560        nonces,
561    };
562    Ok((reply, state))
563}
564
565pub fn follower_round2(
566    config: &Config,
567    state: FollowerState,
568    coordinator: CoordinatorRound2,
569) -> Result<FollowerRound2> {
570    let to_be_signed = state.request.to_be_signed(config)?;
571    let signing_packages = coordinator
572        .all_commitments
573        .into_iter()
574        .map(|tree| frost::SigningPackage::new(tree, to_be_signed.as_ref()));
575
576    match state.request {
577        SigningRequest::TransactionPlan(plan) => {
578            let shares = plan
579                .spend_plans()
580                .map(|x| x.randomizer)
581                .chain(plan.delegator_vote_plans().map(|x| x.randomizer))
582                .zip(signing_packages)
583                .zip(state.nonces.into_iter())
584                .map(|((randomizer, signing_package), signer_nonces)| {
585                    frost::round2::sign_randomized(
586                        &signing_package,
587                        &signer_nonces,
588                        &config.key_package(),
589                        randomizer,
590                    )
591                })
592                .collect::<Result<_, _>>()?;
593            Ok(FollowerRound2::make(config.signing_key(), shares))
594        }
595        SigningRequest::ValidatorDefinition(_) | SigningRequest::ValidatorVote(_) => {
596            let shares = signing_packages
597                .zip(state.nonces.into_iter())
598                .map(|(signing_package, signer_nonces)| {
599                    frost::round2::sign(&signing_package, &signer_nonces, &config.key_package())
600                })
601                .collect::<Result<_, _>>()?;
602            Ok(FollowerRound2::make(config.signing_key(), shares))
603        }
604    }
605}