penumbra_sdk_shielded_pool/
backref.rs

1use anyhow::Result;
2use chacha20poly1305::{
3    aead::{Aead, NewAead},
4    ChaCha20Poly1305, Nonce,
5};
6
7use penumbra_sdk_keys::BackreferenceKey;
8use penumbra_sdk_sct::Nullifier;
9use penumbra_sdk_tct as tct;
10
11pub const ENCRYPTED_BACKREF_LEN: usize = 48;
12
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct Backref {
15    note_commitment: tct::StateCommitment,
16}
17
18#[derive(Clone, Debug)]
19pub struct EncryptedBackref {
20    /// The inner bytes can either have 0 or `ENCRYPTED_BACKREF_LEN` bytes.
21    bytes: Vec<u8>,
22}
23
24impl Backref {
25    pub fn new(note_commitment: tct::StateCommitment) -> Self {
26        Self { note_commitment }
27    }
28
29    pub fn encrypt(&self, brk: &BackreferenceKey, nullifier: &Nullifier) -> EncryptedBackref {
30        let cipher = ChaCha20Poly1305::new(&brk.0);
31
32        // Nonce is the first 12 bytes of the nullifier
33        let nonce_bytes = &nullifier.to_bytes()[..12];
34        let nonce = Nonce::from_slice(&nonce_bytes);
35
36        let plaintext = self.note_commitment.0.to_bytes();
37
38        let ciphertext = cipher
39            .encrypt(nonce, plaintext.as_ref())
40            .expect("encryption should succeed  ");
41
42        EncryptedBackref { bytes: ciphertext }
43    }
44}
45
46impl EncryptedBackref {
47    pub fn is_empty(&self) -> bool {
48        self.bytes.is_empty()
49    }
50
51    pub fn len(&self) -> usize {
52        self.bytes.len()
53    }
54
55    pub fn dummy() -> Self {
56        Self { bytes: vec![] }
57    }
58
59    /// Decrypts the encrypted backref, returning a backref if the decryption is successful,
60    /// or `None` if the encrypted backref is zero-length.
61    pub fn decrypt(
62        &self,
63        brk: &BackreferenceKey,
64        nullifier: &Nullifier,
65    ) -> Result<Option<Backref>> {
66        // We might have a 0-length encrypted backref, which
67        // is treated as a valid value and means that the note has no backref.
68        if self.is_empty() {
69            return Ok(None);
70        }
71
72        let cipher = ChaCha20Poly1305::new(&brk.0);
73
74        let nonce_bytes = &nullifier.to_bytes()[..12];
75        let nonce = Nonce::from_slice(&nonce_bytes);
76
77        let plaintext = cipher
78            .decrypt(nonce, self.bytes.as_ref())
79            .map_err(|_| anyhow::anyhow!("decryption error"))?;
80
81        let note_commitment_bytes: [u8; 32] = plaintext
82            .try_into()
83            .map_err(|_| anyhow::anyhow!("decryption error"))?;
84
85        let backref = Backref::try_from(note_commitment_bytes)
86            .map_err(|_| anyhow::anyhow!("decryption error"))?;
87
88        Ok(Some(backref))
89    }
90}
91
92impl TryFrom<[u8; 32]> for Backref {
93    type Error = anyhow::Error;
94
95    fn try_from(bytes: [u8; 32]) -> Result<Self> {
96        Ok(Self {
97            note_commitment: tct::StateCommitment::try_from(bytes)
98                .map_err(|_| anyhow::anyhow!("invalid note commitment"))?,
99        })
100    }
101}
102
103// EncryptedBackrefs can either have 0 or ENCRYPTED_BACKREF_LEN bytes.
104
105impl TryFrom<[u8; ENCRYPTED_BACKREF_LEN]> for EncryptedBackref {
106    type Error = anyhow::Error;
107
108    fn try_from(bytes: [u8; ENCRYPTED_BACKREF_LEN]) -> Result<Self> {
109        Ok(Self {
110            bytes: bytes.to_vec(),
111        })
112    }
113}
114
115impl TryFrom<[u8; 0]> for EncryptedBackref {
116    type Error = anyhow::Error;
117
118    fn try_from(bytes: [u8; 0]) -> Result<Self> {
119        Ok(Self {
120            bytes: bytes.to_vec(),
121        })
122    }
123}
124
125impl From<EncryptedBackref> for Vec<u8> {
126    fn from(encrypted_backref: EncryptedBackref) -> Vec<u8> {
127        encrypted_backref.bytes
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use proptest::prelude::*;
135
136    use penumbra_sdk_asset::{asset, Value};
137    use penumbra_sdk_keys::keys::{Bip44Path, SeedPhrase, SpendKey};
138
139    use crate::{Note, Rseed};
140
141    proptest! {
142        #[test]
143        fn encrypted_backref_zero_length(seed_phrase_randomness in any::<[u8; 32]>(), amount_to_send in any::<u64>(), rseed_randomness in any::<[u8; 32]>()) {
144            let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
145            let sk = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
146            let fvk = sk.full_viewing_key();
147            let brk = fvk.backref_key();
148
149            let ivk = fvk.incoming();
150            let (sender, _dtk_d) = ivk.payment_address(0u32.into());
151
152            let value_to_send = Value {
153                amount: amount_to_send.into(),
154                asset_id: asset::Cache::with_known_assets()
155                    .get_unit("upenumbra")
156                    .unwrap()
157                    .id(),
158            };
159            let rseed = Rseed(rseed_randomness);
160
161            let note = Note::from_parts(sender, value_to_send, rseed).expect("valid note");
162            let note_commitment: penumbra_sdk_tct::StateCommitment = note.commit();
163            let nk = *sk.nullifier_key();
164            let mut sct = tct::Tree::new();
165
166            sct.insert(tct::Witness::Keep, note_commitment).unwrap();
167            let state_commitment_proof = sct.witness(note_commitment).unwrap();
168            let nullifier = Nullifier::derive(&nk, state_commitment_proof.position(), &note_commitment);
169
170            let encrypted_backref = EncryptedBackref::dummy();
171            assert!(encrypted_backref.is_empty());
172            assert_eq!(encrypted_backref.len(), 0);
173
174            // Decrypting a zero-length encrypted backref should return `None`.
175            let decrypted_backref = encrypted_backref.decrypt(&brk, &nullifier).unwrap();
176            assert_eq!(decrypted_backref, None);
177        }
178    }
179
180    proptest! {
181        #[test]
182        fn encrypted_backref_round_trip(seed_phrase_randomness in any::<[u8; 32]>(), amount_to_send in any::<u64>(), rseed_randomness in any::<[u8; 32]>()) {
183            let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
184            let sk = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
185            let fvk = sk.full_viewing_key();
186            let brk = fvk.backref_key();
187
188            let ivk = fvk.incoming();
189            let (sender, _dtk_d) = ivk.payment_address(0u32.into());
190
191            let value_to_send = Value {
192                amount: amount_to_send.into(),
193                asset_id: asset::Cache::with_known_assets()
194                    .get_unit("upenumbra")
195                    .unwrap()
196                    .id(),
197            };
198            let rseed = Rseed(rseed_randomness);
199
200            let note = Note::from_parts(sender, value_to_send, rseed).expect("valid note");
201            let note_commitment: penumbra_sdk_tct::StateCommitment = note.commit();
202            let nk = *sk.nullifier_key();
203            let mut sct = tct::Tree::new();
204
205            sct.insert(tct::Witness::Keep, note_commitment).unwrap();
206            let state_commitment_proof = sct.witness(note_commitment).unwrap();
207            let nullifier = Nullifier::derive(&nk, state_commitment_proof.position(), &note_commitment);
208
209            let backref = Backref::new(note_commitment);
210            let encrypted_backref = backref.encrypt(&brk, &nullifier);
211
212            let decrypted_backref = encrypted_backref.decrypt(&brk, &nullifier).unwrap();
213
214            assert_eq!(Some(backref), decrypted_backref);
215        }
216    }
217}