penumbra_sdk_keys/keys/
ivk.rs

1use ark_ff::{PrimeField, Zero};
2use rand_core::{CryptoRng, RngCore};
3
4use ark_r1cs_std::prelude::*;
5use ark_relations::r1cs::SynthesisError;
6use decaf377::{
7    r1cs::{ElementVar, FqVar},
8    Fq, Fr,
9};
10
11use super::{AddressIndex, Diversifier, DiversifierKey};
12use crate::{
13    fmd, ka,
14    keys::{AuthorizationKeyVar, NullifierKeyVar, IVK_DOMAIN_SEP},
15    prf, Address,
16};
17
18pub const IVK_LEN_BYTES: usize = 64;
19const MOD_R_QUOTIENT: usize = 4;
20
21/// Allows viewing incoming notes, i.e., notes sent to the spending key this
22/// key is derived from.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct IncomingViewingKey {
25    pub(super) ivk: ka::Secret,
26    pub(super) dk: DiversifierKey,
27}
28
29impl IncomingViewingKey {
30    /// Derive a shielded payment address with the given [`AddressIndex`].
31    pub fn payment_address(&self, index: AddressIndex) -> (Address, fmd::DetectionKey) {
32        let d = self.dk.diversifier_for_index(&index);
33        let g_d = d.diversified_generator();
34        let pk_d = self.ivk.diversified_public(&g_d);
35
36        let dtk_d = fmd::DetectionKey::from_field(Fr::from_le_bytes_mod_order(
37            prf::expand(b"PenumbraExpndFMD", &self.ivk.to_bytes(), d.as_ref()).as_bytes(),
38        ));
39        let ck_d = dtk_d.clue_key();
40
41        (
42            Address::from_components(d, pk_d, ck_d).expect("pk_d is valid"),
43            dtk_d,
44        )
45    }
46
47    /// Derive the (encoding of the) transparent address for the given IVK.
48    ///
49    /// This intentionally returns a `String` rather than an `Address`, as it's not
50    /// safe to truncate arbitrary addresses.
51    pub fn transparent_address(&self) -> String {
52        // The transparent address uses an all-zero diversifier.
53        let dzero = Diversifier([0u8; 16]);
54        let g_dzero = dzero.diversified_generator();
55        let pk_dzero = self.ivk.diversified_public(&g_dzero);
56        let ck_id = fmd::ClueKey([0u8; 32]);
57
58        let address = Address::from_components(dzero, pk_dzero, ck_id).expect("valid address");
59
60        // This should never fail as we just constructed a valid transparent address
61        address
62            .encode_as_transparent_address()
63            .expect("address meets transparent requirements")
64    }
65
66    /// Derive an ephemeral address for the provided account.
67    pub fn ephemeral_address<R: RngCore + CryptoRng>(
68        &self,
69        mut rng: R,
70        mut address_index: AddressIndex,
71    ) -> (Address, fmd::DetectionKey) {
72        let mut random_index = [0u8; 12];
73
74        rng.fill_bytes(&mut random_index);
75
76        address_index.randomizer = random_index;
77
78        self.payment_address(address_index)
79    }
80
81    /// Perform key agreement with a given public key.
82    pub fn key_agreement_with(&self, pk: &ka::Public) -> Result<ka::SharedSecret, ka::Error> {
83        self.ivk.key_agreement_with(pk)
84    }
85
86    /// Derive a transmission key from the given diversified base.
87    pub fn diversified_public(&self, diversified_generator: &decaf377::Element) -> ka::Public {
88        self.ivk.diversified_public(diversified_generator)
89    }
90
91    /// Returns the index used to create the given diversifier (if it was
92    /// created using this incoming viewing key)
93    pub fn index_for_diversifier(&self, diversifier: &Diversifier) -> AddressIndex {
94        self.dk.index_for_diversifier(diversifier)
95    }
96
97    /// Check whether this address is viewable by this incoming viewing key.
98    pub fn views_address(&self, address: &Address) -> bool {
99        self.ivk.diversified_public(address.diversified_generator()) == *address.transmission_key()
100    }
101
102    /// Returns the index of the given address, if the address is viewed by this
103    /// viewing key; otherwise, returns `None`.
104    // TODO: re-evaluate relative to FVK methods
105    pub(super) fn address_index(&self, address: &Address) -> Option<AddressIndex> {
106        if self.views_address(address) {
107            Some(self.index_for_diversifier(address.diversifier()))
108        } else {
109            None
110        }
111    }
112}
113
114pub struct IncomingViewingKeyVar {
115    inner: FqVar,
116}
117
118impl IncomingViewingKeyVar {
119    /// Derive the incoming viewing key from the nk and the ak.
120    pub fn derive(nk: &NullifierKeyVar, ak: &AuthorizationKeyVar) -> Result<Self, SynthesisError> {
121        let cs = nk.inner.cs();
122        let ivk_domain_sep = FqVar::new_constant(cs.clone(), *IVK_DOMAIN_SEP)?;
123        let ivk_mod_q = poseidon377::r1cs::hash_2(
124            cs.clone(),
125            &ivk_domain_sep,
126            (nk.inner.clone(), ak.inner.compress_to_field()?),
127        )?;
128
129        // OOC: Reduce `ivk_mod_q` modulo r
130        let r_modulus: Fq = Fq::from(Fr::MODULUS);
131        let ivk_mod_q_ooc: Fq = ivk_mod_q.value().unwrap_or_default();
132        let ivk_mod_r_ooc = Fr::from_le_bytes_mod_order(&ivk_mod_q_ooc.to_bytes());
133
134        // We also need ivk reduced mod r as an Fq for inserting back into the circuit
135        let ivk_mod_r_ooc_q = Fq::from_le_bytes_mod_order(&ivk_mod_r_ooc.to_bytes());
136        let ivk_mod_r = FqVar::new_witness(cs.clone(), || Ok(ivk_mod_r_ooc_q))?;
137
138        // Finally, we figure out how many times we needed to subtract r from ivk_mod_q_ooc to get ivk_mod_r_ooc.
139        let mut temp_ivk_mod_q = ivk_mod_q_ooc;
140        let mut a = 0;
141        while temp_ivk_mod_q > r_modulus {
142            temp_ivk_mod_q -= r_modulus;
143            a += 1;
144        }
145
146        // Now we add constraints to demonstrate that `ivk_mod_r` is the correct
147        // reduction from `ivk_mod_q`.
148        //
149        // Constrain: ivk_mod_q = mod_r * a + ivk_mod_r
150        let mod_r_var = FqVar::new_constant(cs.clone(), r_modulus)?;
151        let a_var = FqVar::new_witness(cs.clone(), || Ok(Fq::from(a as u64)))?;
152        let rhs = &mod_r_var * &a_var + &ivk_mod_r;
153        ivk_mod_q.enforce_equal(&rhs)?;
154
155        // Constrain: a <= 4
156        //
157        // We could use `enforce_cmp` to add an a <= 4 constraint, but it's cheaper
158        // to add constraints to demonstrate a(a-1)(a-2)(a-3)(a-4) = 0.
159        let mut mul = a_var.clone();
160        for i in 1..=MOD_R_QUOTIENT {
161            mul *= a_var.clone() - FqVar::new_constant(cs.clone(), Fq::from(i as u64))?;
162        }
163        let zero = FqVar::new_constant(cs.clone(), Fq::zero())?;
164        mul.enforce_equal(&zero)?;
165
166        // Constrain: ivk_mod_r < r
167        // Here we can use the existing `enforce_cmp` method on FqVar as r <= (q-1)/2.
168        ivk_mod_r.enforce_cmp(&mod_r_var, core::cmp::Ordering::Less, false)?;
169
170        // Constraint: a = 4 => ivk_mod_r < q - 4 * mod_r
171        let is_less_than_q_minus_4_mod_r = ivk_mod_r.is_cmp(
172            &FqVar::new_constant(
173                cs.clone(),
174                -Fq::from(MOD_R_QUOTIENT as u64) * Fq::from(r_modulus),
175            )?,
176            core::cmp::Ordering::Less,
177            false,
178        )?;
179        let overflows = a_var
180            .is_eq(&FqVar::new_constant(
181                cs.clone(),
182                &Fq::from(MOD_R_QUOTIENT as u64),
183            )?)?
184            .and(&is_less_than_q_minus_4_mod_r.not())?;
185        overflows.enforce_equal(&Boolean::FALSE)?;
186
187        Ok(IncomingViewingKeyVar { inner: ivk_mod_r })
188    }
189
190    /// Derive a transmission key from the given diversified base.
191    pub fn diversified_public(
192        &self,
193        diversified_generator: &ElementVar,
194    ) -> Result<ElementVar, SynthesisError> {
195        let ivk_vars = self.inner.to_bits_le()?;
196        diversified_generator.scalar_mul_le(ivk_vars.to_bits_le()?.iter())
197    }
198}
199
200#[cfg(test)]
201mod test {
202    use crate::{
203        keys::{Bip44Path, SeedPhrase, SpendKey},
204        test_keys,
205    };
206    use proptest::prelude::*;
207    use std::str::FromStr;
208
209    use super::*;
210
211    #[test]
212    fn transparent_address_generation_and_parsing() {
213        // Use test seed phrase for test vector
214        let seed_phrase = SeedPhrase::from_str(test_keys::SEED_PHRASE).expect("valid seed phrase");
215        let spend_key = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
216        let ivk = spend_key.full_viewing_key().incoming();
217
218        let transparent_address_str = ivk.transparent_address();
219
220        let reconstructed: Address = transparent_address_str
221            .parse()
222            .expect("can parse transparent address");
223
224        assert!(ivk.views_address(&reconstructed));
225
226        let address_index = ivk.address_index(&reconstructed).expect("views address");
227
228        let actual_address = ivk.payment_address(address_index).0;
229
230        // The diversifiers will not match, as the encryption of the 0 account `AddressIndex`
231        // is not the null ciphertext, so when deriving `actual_address` from the 0 account
232        // `AddressIndex`, we end up with a different diversifier.
233        assert_ne!(reconstructed.diversifier(), actual_address.diversifier());
234        // The transmission keys also will not match, as the null diversifier is not the
235        // same as the diversifier for the 0 account `AddressIndex`.
236        assert_ne!(
237            reconstructed.transmission_key(),
238            actual_address.transmission_key()
239        );
240        // The clue keys should not match, as the clue key is zeroed out
241        assert_ne!(reconstructed.clue_key(), actual_address.clue_key());
242
243        println!("Transparent address: {}", transparent_address_str);
244        println!("Reconstructed address: {}", reconstructed);
245        println!("Address index: {:?}", address_index);
246        println!("Actual address for index: {}", actual_address);
247    }
248
249    #[test]
250    fn views_address_succeeds_on_own_address() {
251        let rng = rand::rngs::OsRng;
252        let spend_key =
253            SpendKey::from_seed_phrase_bip44(SeedPhrase::generate(rng), &Bip44Path::new(0));
254        let ivk = spend_key.full_viewing_key().incoming();
255        let own_address = ivk.payment_address(AddressIndex::from(0u32)).0;
256        assert!(ivk.views_address(&own_address));
257    }
258
259    proptest! {
260        #[test]
261        fn views_address_succeeds_on_own_ephemeral_address(address_index in any::<u32>()) {
262            let rng = rand::rngs::OsRng;
263            let spend_key = SpendKey::from_seed_phrase_bip44(SeedPhrase::generate(rng), &Bip44Path::new(0));
264            let fvk = spend_key.full_viewing_key();
265            let (own_address, _) = fvk.ephemeral_address(rng, AddressIndex::from(address_index));
266            let ivk = fvk.incoming();
267            assert!(ivk.views_address(&own_address));
268
269            let derived_address_index = fvk.address_index(&own_address);
270            assert_eq!(derived_address_index.expect("index exists").account, AddressIndex::from(address_index).account);
271        }
272    }
273
274    #[test]
275    fn views_address_fails_on_other_address() {
276        let rng = rand::rngs::OsRng;
277        let spend_key =
278            SpendKey::from_seed_phrase_bip44(SeedPhrase::generate(rng), &Bip44Path::new(0));
279        let ivk = spend_key.full_viewing_key().incoming();
280
281        let other_address =
282            SpendKey::from_seed_phrase_bip44(SeedPhrase::generate(rng), &Bip44Path::new(0))
283                .full_viewing_key()
284                .incoming()
285                .payment_address(AddressIndex::from(0u32))
286                .0;
287
288        assert!(!ivk.views_address(&other_address));
289    }
290
291    #[test]
292    fn enforce_field_assumptions() {
293        use num_bigint::BigUint;
294        use num_traits::ops::checked::CheckedSub;
295
296        let fq_modulus: BigUint = Fq::MODULUS.into();
297        let max_q: BigUint = &fq_modulus - 1u32;
298        let fr_modulus: BigUint = Fr::MODULUS.into();
299        assert!(
300            fr_modulus < fq_modulus,
301            "we assume that our scalar field is smaller than our base field"
302        );
303
304        let mut multiple = 0;
305        let mut res = max_q;
306        loop {
307            res = if let Some(x) = res.checked_sub(&fr_modulus) {
308                multiple += 1;
309                x
310            } else {
311                break;
312            };
313        }
314
315        assert_eq!(
316            MOD_R_QUOTIENT, multiple,
317            "`a = fr_modulus * 4 + r mod q` only works on specific curve parameters"
318        );
319    }
320}