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() + plan.delegator_vote_plans().count()
325 }
326 SigningRequest::ValidatorDefinition(_) => 1,
327 SigningRequest::ValidatorVote(_) => 1,
328 }
329}
330
331pub 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 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}