penumbra_sdk_shielded_pool/
backref.rs1use 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 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 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 pub fn decrypt(
62 &self,
63 brk: &BackreferenceKey,
64 nullifier: &Nullifier,
65 ) -> Result<Option<Backref>> {
66 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
103impl 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(), ¬e_commitment);
169
170 let encrypted_backref = EncryptedBackref::dummy();
171 assert!(encrypted_backref.is_empty());
172 assert_eq!(encrypted_backref.len(), 0);
173
174 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(), ¬e_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}