tpe/
lib.rs

1use std::collections::BTreeMap;
2use std::io::Write;
3use std::ops::Mul;
4
5use bitcoin_hashes::{sha256, Hash};
6use bls12_381::{pairing, G1Projective, G2Projective, Scalar};
7pub use bls12_381::{G1Affine, G2Affine};
8use fedimint_core::bls12_381_serde;
9use fedimint_core::encoding::{Decodable, Encodable};
10use group::ff::Field;
11use group::{Curve, Group};
12use rand_chacha::rand_core::SeedableRng;
13use rand_chacha::ChaChaRng;
14use serde::{Deserialize, Serialize};
15
16#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
17pub struct SecretKeyShare(#[serde(with = "bls12_381_serde::scalar")] pub Scalar);
18
19#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
20pub struct PublicKeyShare(#[serde(with = "bls12_381_serde::g1")] pub G1Affine);
21
22#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
23pub struct AggregatePublicKey(#[serde(with = "bls12_381_serde::g1")] pub G1Affine);
24
25#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
26pub struct DecryptionKeyShare(#[serde(with = "bls12_381_serde::g1")] pub G1Affine);
27
28#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
29pub struct AggregateDecryptionKey(#[serde(with = "bls12_381_serde::g1")] pub G1Affine);
30
31#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
32pub struct EphemeralPublicKey(#[serde(with = "bls12_381_serde::g1")] pub G1Affine);
33
34#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
35pub struct EphemeralSignature(#[serde(with = "bls12_381_serde::g2")] pub G2Affine);
36
37#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Encodable, Decodable, Serialize, Deserialize)]
38pub struct CipherText {
39    #[serde(with = "serde_big_array::BigArray")]
40    pub encrypted_preimage: [u8; 32],
41    pub pk: EphemeralPublicKey,
42    pub signature: EphemeralSignature,
43}
44
45pub fn derive_pk_share(sk: &SecretKeyShare) -> PublicKeyShare {
46    PublicKeyShare(G1Projective::generator().mul(sk.0).to_affine())
47}
48
49pub fn encrypt_preimage(
50    agg_pk: &AggregatePublicKey,
51    encryption_seed: &[u8; 32],
52    preimage: &[u8; 32],
53    commitment: &sha256::Hash,
54) -> CipherText {
55    let agg_dk = derive_agg_dk(agg_pk, encryption_seed);
56    let encrypted_preimage = xor_with_hash(*preimage, &agg_dk);
57
58    let ephemeral_sk = derive_ephemeral_sk(encryption_seed);
59    let ephemeral_pk = G1Projective::generator().mul(ephemeral_sk).to_affine();
60    let ephemeral_signature = hash_to_message(&encrypted_preimage, &ephemeral_pk, commitment)
61        .mul(ephemeral_sk)
62        .to_affine();
63
64    CipherText {
65        encrypted_preimage,
66        pk: EphemeralPublicKey(ephemeral_pk),
67        signature: EphemeralSignature(ephemeral_signature),
68    }
69}
70
71pub fn derive_agg_dk(
72    agg_pk: &AggregatePublicKey,
73    encryption_seed: &[u8; 32],
74) -> AggregateDecryptionKey {
75    AggregateDecryptionKey(
76        agg_pk
77            .0
78            .mul(derive_ephemeral_sk(encryption_seed))
79            .to_affine(),
80    )
81}
82
83fn derive_ephemeral_sk(encryption_seed: &[u8; 32]) -> Scalar {
84    Scalar::random(&mut ChaChaRng::from_seed(*encryption_seed))
85}
86
87fn xor_with_hash(mut bytes: [u8; 32], agg_dk: &AggregateDecryptionKey) -> [u8; 32] {
88    let hash = sha256::Hash::hash(&agg_dk.0.to_compressed());
89
90    for i in 0..32 {
91        bytes[i] ^= hash[i];
92    }
93
94    bytes
95}
96
97fn hash_to_message(
98    encrypted_point: &[u8; 32],
99    ephemeral_pk: &G1Affine,
100    commitment: &sha256::Hash,
101) -> G2Affine {
102    let mut engine = sha256::HashEngine::default();
103
104    engine
105        .write_all("FEDIMINT_TPE_BLS12_381_MESSAGE".as_bytes())
106        .expect("Writing to a hash engine cannot fail");
107
108    engine
109        .write_all(encrypted_point)
110        .expect("Writing to a hash engine cannot fail");
111
112    engine
113        .write_all(&ephemeral_pk.to_compressed())
114        .expect("Writing to a hash engine cannot fail");
115
116    engine
117        .write_all(commitment.as_byte_array())
118        .expect("Writing to a hash engine cannot fail");
119
120    let seed = sha256::Hash::from_engine(engine).to_byte_array();
121
122    G2Projective::random(&mut ChaChaRng::from_seed(seed)).to_affine()
123}
124
125/// Verifying a ciphertext guarantees that it has not been malleated.
126pub fn verify_ciphertext(ct: &CipherText, commitment: &sha256::Hash) -> bool {
127    let message = hash_to_message(&ct.encrypted_preimage, &ct.pk.0, commitment);
128
129    pairing(&G1Affine::generator(), &ct.signature.0) == pairing(&ct.pk.0, &message)
130}
131
132pub fn decrypt_preimage(ct: &CipherText, agg_dk: &AggregateDecryptionKey) -> [u8; 32] {
133    xor_with_hash(ct.encrypted_preimage, agg_dk)
134}
135
136/// The function asserts that the ciphertext is valid.
137pub fn verify_agg_dk(
138    agg_pk: &AggregatePublicKey,
139    agg_dk: &AggregateDecryptionKey,
140    ct: &CipherText,
141    commitment: &sha256::Hash,
142) -> bool {
143    let message = hash_to_message(&ct.encrypted_preimage, &ct.pk.0, commitment);
144
145    assert_eq!(
146        pairing(&G1Affine::generator(), &ct.signature.0),
147        pairing(&ct.pk.0, &message)
148    );
149
150    // Since the ciphertext is valid its signature is the ecdh point of the message
151    // and the ephemeral public key. Hence, the following equation holds if and only
152    // if the aggregate decryption key is the ecdh point of the ephemeral public key
153    // and the aggregate public key.
154
155    pairing(&agg_dk.0, &message) == pairing(&agg_pk.0, &ct.signature.0)
156}
157
158pub fn create_dk_share(sks: &SecretKeyShare, ct: &CipherText) -> DecryptionKeyShare {
159    DecryptionKeyShare(ct.pk.0.mul(sks.0).to_affine())
160}
161
162/// The function asserts that the ciphertext is valid.
163pub fn verify_dk_share(
164    pks: &PublicKeyShare,
165    dks: &DecryptionKeyShare,
166    ct: &CipherText,
167    commitment: &sha256::Hash,
168) -> bool {
169    let message = hash_to_message(&ct.encrypted_preimage, &ct.pk.0, commitment);
170
171    assert_eq!(
172        pairing(&G1Affine::generator(), &ct.signature.0),
173        pairing(&ct.pk.0, &message)
174    );
175
176    // Since the ciphertext is valid its signature is the ecdh point of the message
177    // and the ephemeral public key. Hence, the following equation holds if and only
178    // if the decryption key share is the ecdh point of the ephemeral public key and
179    // the public key share.
180
181    pairing(&dks.0, &message) == pairing(&pks.0, &ct.signature.0)
182}
183
184pub fn aggregate_dk_shares(shares: &BTreeMap<u64, DecryptionKeyShare>) -> AggregateDecryptionKey {
185    AggregateDecryptionKey(
186        lagrange_multipliers(
187            shares
188                .keys()
189                .cloned()
190                .map(|peer| Scalar::from(peer + 1))
191                .collect(),
192        )
193        .into_iter()
194        .zip(shares.values())
195        .map(|(lagrange_multiplier, share)| lagrange_multiplier * share.0)
196        .reduce(|a, b| a + b)
197        .expect("We have at least one share")
198        .to_affine(),
199    )
200}
201
202fn lagrange_multipliers(scalars: Vec<Scalar>) -> Vec<Scalar> {
203    scalars
204        .iter()
205        .map(|i| {
206            scalars
207                .iter()
208                .filter(|j| *j != i)
209                .map(|j| j * (j - i).invert().expect("We filtered the case j == i"))
210                .reduce(|a, b| a * b)
211                .expect("We have at least one share")
212        })
213        .collect()
214}
215
216macro_rules! impl_hash_with_serialized_compressed {
217    ($type:ty) => {
218        impl std::hash::Hash for $type {
219            fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
220                state.write(&self.0.to_compressed());
221            }
222        }
223    };
224}
225
226impl_hash_with_serialized_compressed!(AggregatePublicKey);
227impl_hash_with_serialized_compressed!(DecryptionKeyShare);
228impl_hash_with_serialized_compressed!(AggregateDecryptionKey);
229impl_hash_with_serialized_compressed!(EphemeralPublicKey);
230impl_hash_with_serialized_compressed!(EphemeralSignature);
231impl_hash_with_serialized_compressed!(PublicKeyShare);
232
233#[cfg(test)]
234mod tests {
235    use bitcoin_hashes::{sha256, Hash};
236    use bls12_381::{G1Projective, Scalar};
237    use group::ff::Field;
238    use group::Curve;
239    use rand::SeedableRng;
240    use rand_chacha::ChaChaRng;
241
242    use crate::{
243        aggregate_dk_shares, create_dk_share, decrypt_preimage, derive_agg_dk, derive_pk_share,
244        encrypt_preimage, verify_agg_dk, verify_ciphertext, verify_dk_share, AggregatePublicKey,
245        PublicKeyShare, SecretKeyShare,
246    };
247
248    fn dealer_agg_pk() -> AggregatePublicKey {
249        AggregatePublicKey((G1Projective::generator() * coefficient(0)).to_affine())
250    }
251
252    fn dealer_pk(threshold: u64, peer: u64) -> PublicKeyShare {
253        derive_pk_share(&dealer_sk(threshold, peer))
254    }
255
256    fn dealer_sk(threshold: u64, peer: u64) -> SecretKeyShare {
257        let x = Scalar::from(peer + 1);
258
259        // We evaluate the scalar polynomial of degree threshold - 1 at the point x
260        // using the Horner schema.
261
262        let y = (0..threshold)
263            .map(coefficient)
264            .rev()
265            .reduce(|accumulator, c| accumulator * x + c)
266            .expect("We have at least one coefficient");
267
268        SecretKeyShare(y)
269    }
270
271    fn coefficient(index: u64) -> Scalar {
272        Scalar::random(&mut ChaChaRng::from_seed(
273            *sha256::Hash::hash(&index.to_be_bytes()).as_byte_array(),
274        ))
275    }
276
277    #[test]
278    fn test_roundtrip() {
279        const PEERS: u64 = 4;
280        const THRESHOLD: u64 = 3;
281
282        let encryption_seed = [7_u8; 32];
283        let preimage = [42_u8; 32];
284        let commitment = sha256::Hash::hash(&[0_u8; 32]);
285        let ct = encrypt_preimage(&dealer_agg_pk(), &encryption_seed, &preimage, &commitment);
286
287        assert!(verify_ciphertext(&ct, &commitment));
288
289        for peer in 0..PEERS {
290            assert!(verify_dk_share(
291                &dealer_pk(THRESHOLD, peer),
292                &create_dk_share(&dealer_sk(THRESHOLD, peer), &ct),
293                &ct,
294                &commitment
295            ));
296        }
297
298        let selected_shares = (0..THRESHOLD)
299            .map(|peer| (peer, create_dk_share(&dealer_sk(THRESHOLD, peer), &ct)))
300            .collect();
301
302        let agg_dk = aggregate_dk_shares(&selected_shares);
303
304        assert_eq!(agg_dk, derive_agg_dk(&dealer_agg_pk(), &encryption_seed));
305
306        assert!(verify_agg_dk(&dealer_agg_pk(), &agg_dk, &ct, &commitment));
307
308        assert_eq!(preimage, decrypt_preimage(&ct, &agg_dk));
309    }
310}