darkfi_sdk/crypto/
schnorr.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19#[cfg(feature = "async")]
20use darkfi_serial::async_trait;
21use darkfi_serial::{SerialDecodable, SerialEncodable};
22use halo2_gadgets::ecc::chip::FixedPoint;
23use pasta_curves::{
24    group::{ff::PrimeField, Group, GroupEncoding},
25    pallas,
26};
27
28use super::{
29    constants::{NullifierK, DRK_SCHNORR_DOMAIN},
30    util::{fp_mod_fv, hash_to_scalar},
31    PublicKey, SecretKey,
32};
33
34/// Schnorr signature with a commit and response
35#[derive(Debug, Clone, Copy, Eq, PartialEq, SerialEncodable, SerialDecodable)]
36pub struct Signature {
37    commit: pallas::Point,
38    response: pallas::Scalar,
39}
40
41impl Signature {
42    /// Return a dummy identity `Signature`
43    pub fn dummy() -> Self {
44        Self { commit: pallas::Point::identity(), response: pallas::Scalar::zero() }
45    }
46}
47
48/// Trait for secret keys that implements a signature creation
49pub trait SchnorrSecret {
50    /// Sign a given message
51    fn sign(&self, message: &[u8]) -> Signature;
52}
53
54/// Trait for public keys that implements a signature verification
55pub trait SchnorrPublic {
56    /// Verify a given message is valid given a signature.
57    fn verify(&self, message: &[u8], signature: &Signature) -> bool;
58}
59
60/// Schnorr signature trait implementations for the stuff in `keypair.rs`
61impl SchnorrSecret for SecretKey {
62    fn sign(&self, message: &[u8]) -> Signature {
63        // Derive a deterministic nonce
64        let mask = hash_to_scalar(DRK_SCHNORR_DOMAIN, &[&self.inner().to_repr(), message]);
65
66        let commit = NullifierK.generator() * mask;
67
68        let commit_bytes = commit.to_bytes();
69        let pubkey_bytes = PublicKey::from_secret(*self).to_bytes();
70        let transcript = &[&commit_bytes, &pubkey_bytes, message];
71
72        let challenge = hash_to_scalar(DRK_SCHNORR_DOMAIN, transcript);
73        let response = mask + challenge * fp_mod_fv(self.inner());
74
75        Signature { commit, response }
76    }
77}
78
79impl SchnorrPublic for PublicKey {
80    fn verify(&self, message: &[u8], signature: &Signature) -> bool {
81        let commit_bytes = signature.commit.to_bytes();
82        let pubkey_bytes = self.to_bytes();
83        let transcript = &[&commit_bytes, &pubkey_bytes, message];
84
85        let challenge = hash_to_scalar(DRK_SCHNORR_DOMAIN, transcript);
86        NullifierK.generator() * signature.response - self.inner() * challenge == signature.commit
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use darkfi_serial::{deserialize, serialize};
94    use rand::rngs::OsRng;
95
96    #[test]
97    fn test_schnorr_signature() {
98        let secret = SecretKey::random(&mut OsRng);
99        let message: &[u8] = b"aaaahhhh i'm signiiinngg";
100        let signature = secret.sign(message);
101        let public = PublicKey::from_secret(secret);
102        assert!(public.verify(message, &signature));
103
104        // Check out if it's also fine with serialization
105        let ser = serialize(&signature);
106        let de = deserialize(&ser).unwrap();
107        assert!(public.verify(message, &de));
108    }
109}