penumbra_sdk_custody/threshold/
dkg.rs

1use anyhow::{anyhow, Result};
2use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
3use decaf377::Fq;
4use decaf377_frost as frost;
5use frost::keys::dkg as frost_dkg;
6use std::collections::{HashMap, HashSet};
7mod encryption;
8use ed25519_consensus::{Signature, SigningKey, VerificationKey};
9use encryption::EncryptionKey;
10use penumbra_sdk_proto::{custody::threshold::v1 as pb, DomainType, Message};
11use rand_core::CryptoRngCore;
12
13use self::encryption::DecryptionKey;
14
15use super::Config;
16
17/// A commitment to our share of the nullifier.
18///
19/// In order to check a commitment, simply use the equality operator, after recreating
20/// the commitment.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22struct NullifierCommitment([u8; 32]);
23
24impl NullifierCommitment {
25    /// Create a new commitment to a given nullifier
26    fn create(share: Fq) -> Self {
27        let mut state = blake2b_simd::Params::new()
28            .personal(b"dkg-commit")
29            .to_state();
30        share
31            .serialize_compressed(&mut state)
32            .expect("failed to serialize Fq element");
33        let out = state.finalize().as_array()[..32]
34            .try_into()
35            .expect("array conversion should not fail");
36        Self(out)
37    }
38
39    fn as_bytes(&self) -> &[u8; 32] {
40        &self.0
41    }
42}
43
44impl TryFrom<&[u8]> for NullifierCommitment {
45    type Error = anyhow::Error;
46
47    fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
48        let bytes: [u8; 32] = value.try_into()?;
49        Ok(Self(bytes))
50    }
51}
52
53/// The message we send in round 1 of the DKG protocol
54#[derive(Clone)]
55pub struct Round1 {
56    /// The underlying FROST package.
57    package: frost_dkg::round1::Package,
58    /// A commitment to our share of the nullifier.
59    nullifier_commitment: NullifierCommitment,
60    /// An encryption key to receive the encrypted packages in round 2.
61    epk: EncryptionKey,
62    /// The verification key that will act as our identity from now on.
63    vk: VerificationKey,
64}
65
66impl From<Round1> for pb::DkgRound1 {
67    fn from(value: Round1) -> Self {
68        Self {
69            pkg: Some(value.package.into()),
70            nullifier_commitment: value.nullifier_commitment.as_bytes().to_vec(),
71            epk: value.epk.as_bytes().to_vec(),
72            vk: value.vk.as_bytes().to_vec(),
73        }
74    }
75}
76
77impl TryFrom<pb::DkgRound1> for Round1 {
78    type Error = anyhow::Error;
79
80    fn try_from(value: pb::DkgRound1) -> std::result::Result<Self, Self::Error> {
81        Ok(Self {
82            package: value
83                .pkg
84                .ok_or(anyhow!("DkgRound1 missing pkg"))?
85                .try_into()?,
86            nullifier_commitment: value.nullifier_commitment.as_slice().try_into()?,
87            epk: value.epk.as_slice().try_into()?,
88            vk: value.vk.as_slice().try_into()?,
89        })
90    }
91}
92
93impl DomainType for Round1 {
94    type Proto = pb::DkgRound1;
95}
96
97fn round2_inner_to_pb(
98    encrypted_packages: HashMap<VerificationKey, Vec<u8>>,
99    nullifier: Fq,
100) -> pb::dkg_round2::Inner {
101    let nullifier = {
102        let mut bytes = Vec::new();
103        nullifier
104            .serialize_compressed(&mut bytes)
105            .expect("field serialization should not fail");
106        bytes
107    };
108    // Need to sort to guarantee a deterministic encoding for signing.
109    let encrypted_packages = {
110        let mut acc: Vec<_> = encrypted_packages
111            .into_iter()
112            .map(|(k, v)| pb::dkg_round2::TargetedPackage {
113                vk: k.as_bytes().to_vec(),
114                encrypted_package: v,
115            })
116            .collect();
117        acc.sort_by_key(|x| x.vk.clone());
118        acc
119    };
120    pb::dkg_round2::Inner {
121        encrypted_packages,
122        nullifier,
123    }
124}
125
126/// The message we send in round 2 of the DKG protocol.
127#[derive(Clone, Debug)]
128pub struct Round2 {
129    /// For each other participant, a ciphertext containing the FROST package they need.
130    encrypted_packages: HashMap<VerificationKey, Vec<u8>>,
131    /// Our revealed nullifier (share) from the previous round.
132    nullifier: Fq,
133    /// A declaration of our identity.
134    vk: VerificationKey,
135    // A signature over the encrypted packages and nullifier.
136    sig: Signature,
137}
138
139impl From<Round2> for pb::DkgRound2 {
140    fn from(value: Round2) -> Self {
141        Self {
142            inner: Some(round2_inner_to_pb(
143                value.encrypted_packages,
144                value.nullifier,
145            )),
146            vk: value.vk.as_bytes().to_vec(),
147            sig: value.sig.to_bytes().to_vec(),
148        }
149    }
150}
151
152impl TryFrom<pb::DkgRound2> for Round2 {
153    type Error = anyhow::Error;
154
155    fn try_from(value: pb::DkgRound2) -> std::result::Result<Self, Self::Error> {
156        let inner = value.inner.ok_or(anyhow!("DkgRound2 missing inner"))?;
157        Ok(Self {
158            encrypted_packages: inner
159                .encrypted_packages
160                .into_iter()
161                .map(|x| Ok((x.vk.as_slice().try_into()?, x.encrypted_package)))
162                .collect::<Result<HashMap<_, _>, Self::Error>>()?,
163            nullifier: Fq::deserialize_compressed(inner.nullifier.as_slice())?,
164            vk: value.vk.as_slice().try_into()?,
165            sig: value.sig.as_slice().try_into()?,
166        })
167    }
168}
169
170impl DomainType for Round2 {
171    type Proto = pb::DkgRound2;
172}
173
174impl Round2 {
175    fn make(
176        sk: &SigningKey,
177        encrypted_packages: HashMap<VerificationKey, Vec<u8>>,
178        nullifier: Fq,
179    ) -> Self {
180        let data = round2_inner_to_pb(encrypted_packages.clone(), nullifier).encode_to_vec();
181        let sig = sk.sign(&data);
182        Self {
183            encrypted_packages,
184            nullifier,
185            vk: sk.verification_key(),
186            sig,
187        }
188    }
189
190    fn encrypted_packages(self) -> Result<(VerificationKey, HashMap<VerificationKey, Vec<u8>>)> {
191        let data =
192            round2_inner_to_pb(self.encrypted_packages.clone(), self.nullifier).encode_to_vec();
193        self.vk.verify(&self.sig, &data)?;
194        Ok((self.vk, self.encrypted_packages))
195    }
196}
197
198/// The state we need to remember after round 1.
199pub struct Round1State {
200    /// This is what FROST tells us to remember.
201    secret_package: frost_dkg::round1::SecretPackage,
202    /// We remember our nullifier share, so we can open it later.
203    nullifier: Fq,
204    /// We remember our signing key, for the final config, and to sign the next round.
205    sk: SigningKey,
206    /// We remember the new decryption key we've created, to decrypt the next round's packages.
207    edk: DecryptionKey,
208}
209
210/// The state we need to remember after round 2.
211pub struct Round2State {
212    /// This is what FROST tells us to remember.
213    secret_package: frost_dkg::round2::SecretPackage,
214    /// FROST round 3 will need this data.
215    round1_packages: HashMap<frost::Identifier, frost_dkg::round1::Package>,
216    /// We want to keep a list of verification keys, and we need to remember the commitments
217    /// so that we can check the openings in the next round anyways.
218    ///
219    /// Caching the identifier mappings here is useful as well.
220    associated_data: HashMap<VerificationKey, (frost::Identifier, NullifierCommitment)>,
221    /// Our share of the nullifier, which we need to sum with the opened nullifiers next round.
222    nullifier: Fq,
223    /// We keep the signing key to save in the config
224    sk: SigningKey,
225    /// We keep this, so that we can decrypt the packages.
226    edk: DecryptionKey,
227}
228
229pub fn round1(mut rng: impl CryptoRngCore, t: u16, n: u16) -> Result<(Round1, Round1State)> {
230    // hack to get around SigningKey taking rng by value
231    let sk = SigningKey::new(&mut rng);
232    let vk = sk.verification_key();
233    let id = frost::Identifier::derive(vk.as_bytes())?;
234    let (secret_package, package) = frost_dkg::part1(id, n, t, &mut rng)?;
235    let edk = DecryptionKey::new(&mut rng);
236    let epk = edk.public();
237    let nullifier = Fq::rand(&mut rng);
238    let nullifier_commitment = NullifierCommitment::create(nullifier);
239    let round1 = Round1 {
240        package,
241        nullifier_commitment,
242        epk,
243        vk,
244    };
245    let state = Round1State {
246        secret_package,
247        nullifier,
248        sk,
249        edk,
250    };
251    Ok((round1, state))
252}
253
254pub fn round2(
255    mut rng: impl CryptoRngCore,
256    state: Round1State,
257    messages: Vec<Round1>,
258) -> Result<(Round2, Round2State)> {
259    // Check that all verification keys are unique, and not equal to my own
260    {
261        let mut seen = HashSet::new();
262        seen.insert(state.sk.verification_key());
263        for m in &messages {
264            if seen.contains(&m.vk) {
265                anyhow::bail!("duplicate verification key in messages");
266            }
267        }
268    }
269
270    let associated_info = messages
271        .into_iter()
272        .map(|x| {
273            Ok((
274                x.vk,
275                (
276                    frost::Identifier::derive(x.vk.as_bytes())?,
277                    x.epk,
278                    x.package,
279                    x.nullifier_commitment,
280                ),
281            ))
282        })
283        .collect::<Result<HashMap<_, _>>>()?;
284    let round1_packages = associated_info
285        .iter()
286        .map(|(_, (id, _, package, _))| (*id, package.clone()))
287        .collect();
288    let (secret_package, output_packages) =
289        frost_dkg::part2(state.secret_package, &round1_packages)?;
290    let round2 = {
291        let encrypted_packages = associated_info
292            .iter()
293            .map(|(vk, (id, epk, _, _))| {
294                let package = output_packages
295                    .get(id)
296                    .ok_or(anyhow!("unknown identifier: {:?}", id))?;
297                let ciphertext = epk.encrypt(&mut rng, &package.encode_to_vec());
298                Ok((vk.clone(), ciphertext))
299            })
300            .collect::<Result<HashMap<_, _>>>()?;
301        Round2::make(&state.sk, encrypted_packages, state.nullifier)
302    };
303    let state = Round2State {
304        secret_package,
305        round1_packages,
306        associated_data: associated_info
307            .into_iter()
308            .map(|(vk, (id, _, _, com))| (vk, (id, com)))
309            .collect(),
310        nullifier: state.nullifier,
311        sk: state.sk,
312        edk: state.edk,
313    };
314    Ok((round2, state))
315}
316
317pub fn round3(
318    mut rng: impl CryptoRngCore,
319    state: Round2State,
320    messages: Vec<Round2>,
321) -> Result<Config> {
322    let nullifier_key = {
323        let mut acc = state.nullifier;
324        for message in &messages {
325            if !state.associated_data.contains_key(&message.vk) {
326                anyhow::bail!("unknown verification key in round 2 message");
327            }
328            if NullifierCommitment::create(message.nullifier)
329                != state.associated_data[&message.vk].1
330            {
331                anyhow::bail!("opened nullifier did not match commitment");
332            }
333            acc += message.nullifier;
334        }
335        acc
336    };
337    let round2_packages = messages
338        .into_iter()
339        .map(|x| {
340            let (vk, encrypted_packages) = x.encrypted_packages()?;
341            let my_ciphertext = encrypted_packages
342                .get(&state.sk.verification_key())
343                .ok_or(anyhow!("no encrypted package for this recipient"))?;
344            let my_plaintext = state.edk.decrypt(&mut rng, &my_ciphertext)?;
345            let package = frost_dkg::round2::Package::decode(my_plaintext.as_slice())?;
346            let id = state
347                .associated_data
348                .get(&vk)
349                .ok_or(anyhow!("unknown verification key in round 2 message"))?
350                .0;
351            Ok((id, package))
352        })
353        .collect::<Result<HashMap<_, _>>>()?;
354    let (key_package, public_key_package) = frost_dkg::part3(
355        &state.secret_package,
356        &state.round1_packages,
357        &round2_packages,
358    )?;
359    let verification_keys = {
360        let mut acc: Vec<_> = state.associated_data.keys().cloned().collect();
361        acc.push(state.sk.verification_key());
362        acc
363    };
364    Ok(Config::from_parts(
365        key_package,
366        public_key_package,
367        state.sk,
368        verification_keys,
369        nullifier_key,
370    ))
371}