Skip to main content

nym_compact_ecash/scheme/
aggregation.rs

1// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::common_types::{PartialSignature, Signature, SignatureShare, SignerIndex};
5use crate::error::{CompactEcashError, Result};
6use crate::helpers::{scalar_date, scalar_type};
7use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth};
8use crate::scheme::withdrawal::RequestInfo;
9use crate::scheme::{PartialWallet, Wallet, WalletSignatures};
10use crate::utils::{check_bilinear_pairing, perform_lagrangian_interpolation_at_origin};
11use crate::{ecash_group_parameters, Attribute};
12use core::iter::Sum;
13use core::ops::Mul;
14use group::Curve;
15use itertools::Itertools;
16use nym_bls12_381_fork::{G2Prepared, G2Projective, Scalar};
17use zeroize::Zeroizing;
18
19pub(crate) trait Aggregatable: Sized {
20    fn aggregate(aggregatable: &[Self], indices: Option<&[SignerIndex]>) -> Result<Self>;
21
22    fn check_unique_indices(indices: &[SignerIndex]) -> bool {
23        // if aggregation is a threshold one, all indices should be unique
24        indices.iter().unique_by(|&index| index).count() == indices.len()
25    }
26}
27
28impl<T> Aggregatable for T
29where
30    T: Sum,
31    for<'a> T: Sum<&'a T>,
32    for<'a> &'a T: Mul<Scalar, Output = T>,
33{
34    fn aggregate(aggregatable: &[T], indices: Option<&[u64]>) -> Result<T> {
35        if aggregatable.is_empty() {
36            return Err(CompactEcashError::AggregationEmptySet);
37        }
38
39        if let Some(indices) = indices {
40            if !Self::check_unique_indices(indices) {
41                return Err(CompactEcashError::AggregationDuplicateIndices);
42            }
43            perform_lagrangian_interpolation_at_origin(indices, aggregatable)
44        } else {
45            // non-threshold
46            Ok(aggregatable.iter().sum())
47        }
48    }
49}
50
51impl Aggregatable for PartialSignature {
52    fn aggregate(sigs: &[PartialSignature], indices: Option<&[u64]>) -> Result<Signature> {
53        // Ensure that we have valid signatures
54        if sigs.is_empty() {
55            return Err(CompactEcashError::AggregationEmptySet);
56        }
57
58        // Check each individual signature for point at infinity
59        for sig in sigs {
60            if bool::from(sig.is_at_infinity()) {
61                return Err(CompactEcashError::IdentitySignature);
62            }
63        }
64        let h = sigs
65            .first()
66            .ok_or(CompactEcashError::AggregationEmptySet)?
67            .sig1();
68
69        // TODO: is it possible to avoid this allocation?
70        let sigmas = sigs.iter().map(|sig| *sig.sig2()).collect::<Vec<_>>();
71        let aggr_sigma = Aggregatable::aggregate(&sigmas, indices)?;
72
73        Ok(Signature {
74            h: *h,
75            s: aggr_sigma,
76        })
77    }
78}
79
80/// Ensures all provided verification keys were generated to verify the same number of attributes.
81fn check_same_key_size(keys: &[VerificationKeyAuth]) -> bool {
82    keys.iter().map(|vk| vk.beta_g1.len()).all_equal()
83        && keys.iter().map(|vk| vk.beta_g2.len()).all_equal()
84}
85
86pub fn aggregate_verification_keys(
87    keys: &[VerificationKeyAuth],
88    indices: Option<&[SignerIndex]>,
89) -> Result<VerificationKeyAuth> {
90    if !check_same_key_size(keys) {
91        return Err(CompactEcashError::AggregationSizeMismatch);
92    }
93    Aggregatable::aggregate(keys, indices)
94}
95
96pub fn aggregate_signature_shares(
97    verification_key: &VerificationKeyAuth,
98    attributes: &[Attribute],
99    shares: &[SignatureShare],
100) -> Result<Signature> {
101    let (signatures, indices): (Vec<_>, Vec<_>) = shares
102        .iter()
103        .map(|share| (*share.signature(), share.index()))
104        .unzip();
105
106    aggregate_signatures(verification_key, attributes, &signatures, Some(&indices))
107}
108
109pub fn aggregate_signatures(
110    verification_key: &VerificationKeyAuth,
111    attributes: &[Attribute],
112    signatures: &[PartialSignature],
113    indices: Option<&[SignerIndex]>,
114) -> Result<Signature> {
115    let params = ecash_group_parameters();
116    // aggregate the signature
117
118    let signature = Aggregatable::aggregate(signatures, indices)?;
119
120    // Ensure the aggregated signature is not an infinity point
121    if bool::from(signature.is_at_infinity()) {
122        return Err(CompactEcashError::IdentitySignature);
123    }
124
125    // Verify the signature
126    let tmp = attributes
127        .iter()
128        .zip(verification_key.beta_g2.iter())
129        .map(|(attr, beta_i)| beta_i * attr)
130        .sum::<G2Projective>();
131
132    if !check_bilinear_pairing(
133        &signature.h.to_affine(),
134        &G2Prepared::from((verification_key.alpha + tmp).to_affine()),
135        &signature.s.to_affine(),
136        params.prepared_miller_g2(),
137    ) {
138        return Err(CompactEcashError::AggregationVerification);
139    }
140    Ok(signature)
141}
142
143pub fn aggregate_wallets(
144    verification_key: &VerificationKeyAuth,
145    sk_user: &SecretKeyUser,
146    wallets: &[PartialWallet],
147    req_info: &RequestInfo,
148) -> Result<Wallet> {
149    // Aggregate partial wallets
150    let signature_shares: Vec<SignatureShare> = wallets
151        .iter()
152        .map(|wallet| SignatureShare::new(*wallet.signature(), wallet.index()))
153        .collect();
154
155    let attributes = Zeroizing::new(vec![
156        sk_user.sk,
157        *req_info.get_v(),
158        *req_info.get_expiration_date(),
159        *req_info.get_t_type(),
160    ]);
161    let aggregated_signature =
162        aggregate_signature_shares(verification_key, &attributes, &signature_shares)?;
163
164    let expiration_date_timestamp = req_info.get_expiration_date();
165    let t_type = req_info.get_t_type();
166
167    Ok(Wallet {
168        signatures: WalletSignatures {
169            sig: aggregated_signature,
170            v: *req_info.get_v(),
171            expiration_date_timestamp: scalar_date(expiration_date_timestamp),
172            t_type: scalar_type(t_type),
173        },
174        tickets_spent: 0,
175    })
176}