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() + plan.delegator_vote_plans().count()
325        }
326        SigningRequest::ValidatorDefinition(_) => 1,
327        SigningRequest::ValidatorVote(_) => 1,
328    }
329}
330
331/// Create a trivial signing response if no signatures are needed.
332pub fn no_signature_response(
333    fvk: &FullViewingKey,
334    request: &SigningRequest,
335) -> Result<Option<SigningResponse>> {
336    match request {
337        SigningRequest::TransactionPlan(plan) if required_signatures(request) == 0 => {
338            Ok(Some(SigningResponse::Transaction(AuthorizationData {
339                effect_hash: Some(plan.effect_hash(fvk)?),
340                spend_auths: Vec::new(),
341                delegator_vote_auths: Vec::new(),
342            })))
343        }
344        _ => Ok(None),
345    }
346}
347
348pub struct CoordinatorState1 {
349    request: SigningRequest,
350    my_round1_reply: FollowerRound1,
351    my_round1_state: FollowerState,
352}
353
354pub struct CoordinatorState2 {
355    request: SigningRequest,
356    my_round2_reply: FollowerRound2,
357    to_be_signed: ToBeSigned,
358    signing_packages: Vec<frost::SigningPackage>,
359}
360
361enum ToBeSigned {
362    EffectHash(EffectHash),
363    ValidatorDefinitionBytes(Vec<u8>),
364    ValidatorVoteBytes(Vec<u8>),
365}
366
367impl SigningRequest {
368    fn to_be_signed(&self, config: &Config) -> Result<ToBeSigned> {
369        let out = match self {
370            SigningRequest::TransactionPlan(plan) => {
371                ToBeSigned::EffectHash(plan.effect_hash(config.fvk())?)
372            }
373            SigningRequest::ValidatorDefinition(validator) => ToBeSigned::ValidatorDefinitionBytes(
374                ProtoValidator::from(validator.clone()).encode_to_vec(),
375            ),
376            SigningRequest::ValidatorVote(vote) => ToBeSigned::ValidatorVoteBytes(
377                ProtoValidatorVoteBody::from(vote.clone()).encode_to_vec(),
378            ),
379        };
380        Ok(out)
381    }
382}
383
384impl AsRef<[u8]> for ToBeSigned {
385    fn as_ref(&self) -> &[u8] {
386        match self {
387            ToBeSigned::EffectHash(x) => x.as_ref(),
388            ToBeSigned::ValidatorDefinitionBytes(x) => x.as_slice(),
389            ToBeSigned::ValidatorVoteBytes(x) => x.as_slice(),
390        }
391    }
392}
393
394pub struct FollowerState {
395    request: SigningRequest,
396    nonces: Vec<frost::round1::SigningNonces>,
397}
398
399pub fn coordinator_round1(
400    rng: &mut impl CryptoRngCore,
401    config: &Config,
402    request: SigningRequest,
403) -> Result<(CoordinatorRound1, CoordinatorState1)> {
404    let message = CoordinatorRound1 {
405        request: request.clone(),
406    };
407    let (my_round1_reply, my_round1_state) = follower_round1(rng, config, message.clone())?;
408    let state = CoordinatorState1 {
409        request,
410        my_round1_reply,
411        my_round1_state,
412    };
413    Ok((message, state))
414}
415
416pub fn coordinator_round2(
417    config: &Config,
418    state: CoordinatorState1,
419    follower_messages: &[FollowerRound1],
420) -> Result<(CoordinatorRound2, CoordinatorState2)> {
421    let mut all_commitments = vec![BTreeMap::new(); required_signatures(&state.request)];
422    for message in follower_messages
423        .iter()
424        .cloned()
425        .chain(iter::once(state.my_round1_reply))
426    {
427        let (pk, commitments) = message.checked_commitments()?;
428        if !config.verification_keys().contains(&pk) {
429            anyhow::bail!("unknown verification key: {:?}", pk);
430        }
431        // The public key acts as the identifier
432        let identifier = frost::Identifier::derive(pk.as_bytes().as_slice())?;
433        for (tree_i, com_i) in all_commitments.iter_mut().zip(commitments.into_iter()) {
434            tree_i.insert(identifier, com_i);
435        }
436    }
437    let reply = CoordinatorRound2 { all_commitments };
438
439    let my_round2_reply = follower_round2(config, state.my_round1_state, reply.clone())?;
440
441    let to_be_signed = state.request.to_be_signed(&config)?;
442
443    let signing_packages = {
444        reply
445            .all_commitments
446            .iter()
447            .map(|tree| frost::SigningPackage::new(tree.clone(), to_be_signed.as_ref()))
448            .collect()
449    };
450    let state = CoordinatorState2 {
451        request: state.request,
452        my_round2_reply,
453        to_be_signed,
454        signing_packages,
455    };
456    Ok((reply, state))
457}
458
459pub fn coordinator_round3(
460    config: &Config,
461    state: CoordinatorState2,
462    follower_messages: &[FollowerRound2],
463) -> Result<SigningResponse> {
464    let mut share_maps: Vec<HashMap<frost::Identifier, frost::round2::SignatureShare>> =
465        vec![HashMap::new(); required_signatures(&state.request)];
466    for message in follower_messages
467        .iter()
468        .cloned()
469        .chain(iter::once(state.my_round2_reply))
470    {
471        let (pk, shares) = message.checked_shares()?;
472        if !config.verification_keys().contains(&pk) {
473            anyhow::bail!("unknown verification key: {:?}", pk);
474        }
475        let identifier = frost::Identifier::derive(pk.as_bytes().as_slice())?;
476        for (map_i, share_i) in share_maps.iter_mut().zip(shares.into_iter()) {
477            map_i.insert(identifier, share_i);
478        }
479    }
480
481    match state.request {
482        SigningRequest::TransactionPlan(plan) => {
483            let mut spend_auths = plan
484                .spend_plans()
485                .map(|x| x.randomizer)
486                .chain(plan.delegator_vote_plans().map(|x| x.randomizer))
487                .zip(share_maps.iter())
488                .zip(state.signing_packages.iter())
489                .map(|((randomizer, share_map), signing_package)| {
490                    frost::aggregate_randomized(
491                        signing_package,
492                        &share_map,
493                        &config.public_key_package(),
494                        randomizer,
495                    )
496                })
497                .collect::<Result<Vec<_>, _>>()?;
498            let delegator_vote_auths = spend_auths.split_off(plan.spend_plans().count());
499            Ok(SigningResponse::Transaction(AuthorizationData {
500                effect_hash: {
501                    let ToBeSigned::EffectHash(effect_hash) = state.to_be_signed else {
502                        unreachable!("transaction plan request has non-effect-hash to be signed");
503                    };
504                    Some(effect_hash)
505                },
506                spend_auths,
507                delegator_vote_auths,
508            }))
509        }
510        SigningRequest::ValidatorDefinition(_) => {
511            let validator_definition_auth = share_maps
512                .get(0)
513                .ok_or_else(|| anyhow!("missing signature for validator definition"))?;
514            Ok(SigningResponse::ValidatorDefinition(frost::aggregate(
515                &state
516                    .signing_packages
517                    .get(0)
518                    .expect("same number of signing packages as signatures"),
519                &validator_definition_auth,
520                &config.public_key_package(),
521            )?))
522        }
523        SigningRequest::ValidatorVote(_) => {
524            let validator_vote_auth = share_maps
525                .get(0)
526                .ok_or_else(|| anyhow!("missing signature for validator vote"))?;
527            Ok(SigningResponse::ValidatorVote(frost::aggregate(
528                &state
529                    .signing_packages
530                    .get(0)
531                    .expect("same number of signing packages as signatures"),
532                &validator_vote_auth,
533                &config.public_key_package(),
534            )?))
535        }
536    }
537}
538
539pub fn follower_round1(
540    rng: &mut impl CryptoRngCore,
541    config: &Config,
542    coordinator: CoordinatorRound1,
543) -> Result<(FollowerRound1, FollowerState)> {
544    let required = required_signatures(&coordinator.request);
545    let (nonces, commitments) = (0..required)
546        .map(|_| frost::round1::commit(&config.key_package().secret_share(), rng))
547        .unzip();
548    let reply = FollowerRound1::make(config.signing_key(), commitments);
549    let state = FollowerState {
550        request: coordinator.request,
551        nonces,
552    };
553    Ok((reply, state))
554}
555
556pub fn follower_round2(
557    config: &Config,
558    state: FollowerState,
559    coordinator: CoordinatorRound2,
560) -> Result<FollowerRound2> {
561    let to_be_signed = state.request.to_be_signed(config)?;
562    let signing_packages = coordinator
563        .all_commitments
564        .into_iter()
565        .map(|tree| frost::SigningPackage::new(tree, to_be_signed.as_ref()));
566
567    match state.request {
568        SigningRequest::TransactionPlan(plan) => {
569            let shares = plan
570                .spend_plans()
571                .map(|x| x.randomizer)
572                .chain(plan.delegator_vote_plans().map(|x| x.randomizer))
573                .zip(signing_packages)
574                .zip(state.nonces.into_iter())
575                .map(|((randomizer, signing_package), signer_nonces)| {
576                    frost::round2::sign_randomized(
577                        &signing_package,
578                        &signer_nonces,
579                        &config.key_package(),
580                        randomizer,
581                    )
582                })
583                .collect::<Result<_, _>>()?;
584            Ok(FollowerRound2::make(config.signing_key(), shares))
585        }
586        SigningRequest::ValidatorDefinition(_) | SigningRequest::ValidatorVote(_) => {
587            let shares = signing_packages
588                .zip(state.nonces.into_iter())
589                .map(|(signing_package, signer_nonces)| {
590                    frost::round2::sign(&signing_package, &signer_nonces, &config.key_package())
591                })
592                .collect::<Result<_, _>>()?;
593            Ok(FollowerRound2::make(config.signing_key(), shares))
594        }
595    }
596}