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#[derive(Debug, Clone)]
29pub struct CoordinatorRound1 {
30 request: SigningRequest,
31}
32
33impl CoordinatorRound1 {
34 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 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#[derive(Debug, Clone)]
160pub struct FollowerRound1 {
161 pub(self) commitments: Vec<frost::round1::SigningCommitments>,
163 pub(self) pk: VerificationKey,
165 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 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 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#[derive(Debug, Clone)]
243pub struct FollowerRound2 {
244 pub(self) shares: Vec<frost::round2::SignatureShare>,
246 pub(self) pk: VerificationKey,
248 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 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 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
318fn 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
333pub 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 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}