1use anyhow::{anyhow, Error, Result};
2
3use ark_r1cs_std::prelude::*;
4use ark_relations::r1cs::SynthesisError;
5use decaf377::r1cs::FqVar;
6use decaf377::Fq;
7use once_cell::sync::Lazy;
8use penumbra_sdk_fee::Fee;
9use penumbra_sdk_proto::{
10 core::keys::v1 as pb_keys, penumbra::core::component::dex::v1 as pb, DomainType,
11};
12use penumbra_sdk_tct::StateCommitment;
13use poseidon377::{hash_1, hash_4, hash_7};
14use rand_core::{CryptoRng, RngCore};
15
16use decaf377_ka as ka;
17use penumbra_sdk_asset::{asset, Value, ValueVar};
18use penumbra_sdk_keys::{keys::OutgoingViewingKey, Address, AddressVar, PayloadKey};
19use penumbra_sdk_num::{Amount, AmountVar};
20use penumbra_sdk_shielded_pool::{Note, Rseed};
21use penumbra_sdk_tct::r1cs::StateCommitmentVar;
22
23use crate::{BatchSwapOutputData, TradingPair, TradingPairVar};
24
25use super::{SwapCiphertext, SwapPayload, DOMAIN_SEPARATOR, SWAP_CIPHERTEXT_BYTES, SWAP_LEN_BYTES};
26
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct SwapPlaintext {
29 pub trading_pair: TradingPair,
31 pub delta_1_i: Amount,
33 pub delta_2_i: Amount,
35 pub claim_fee: Fee,
37 pub claim_address: Address,
39 pub rseed: Rseed,
41}
42
43pub static OUTPUT_1_BLINDING_DOMAIN_SEPARATOR: Lazy<Fq> = Lazy::new(|| {
44 Fq::from_le_bytes_mod_order(
45 blake2b_simd::blake2b(b"penumbra.swapclaim.output1.blinding").as_bytes(),
46 )
47});
48pub static OUTPUT_2_BLINDING_DOMAIN_SEPARATOR: Lazy<Fq> = Lazy::new(|| {
49 Fq::from_le_bytes_mod_order(
50 blake2b_simd::blake2b(b"penumbra.swapclaim.output2.blinding").as_bytes(),
51 )
52});
53
54impl SwapPlaintext {
55 pub fn output_rseeds(&self) -> (Rseed, Rseed) {
56 let fq_rseed = Fq::from_le_bytes_mod_order(&self.rseed.to_bytes()[..]);
57 let rseed_1_hash = hash_1(&OUTPUT_1_BLINDING_DOMAIN_SEPARATOR, fq_rseed);
58 let rseed_2_hash = hash_1(&OUTPUT_2_BLINDING_DOMAIN_SEPARATOR, fq_rseed);
59 (
60 Rseed(rseed_1_hash.to_bytes()),
61 Rseed(rseed_2_hash.to_bytes()),
62 )
63 }
64
65 pub fn output_notes(&self, batch_data: &BatchSwapOutputData) -> (Note, Note) {
66 let (output_1_rseed, output_2_rseed) = self.output_rseeds();
67
68 let (lambda_1_i, lambda_2_i) =
69 batch_data.pro_rata_outputs((self.delta_1_i, self.delta_2_i));
70
71 let output_1_note = Note::from_parts(
72 self.claim_address.clone(),
73 Value {
74 amount: lambda_1_i,
75 asset_id: self.trading_pair.asset_1(),
76 },
77 output_1_rseed,
78 )
79 .expect("claim address is valid");
80
81 let output_2_note = Note::from_parts(
82 self.claim_address.clone(),
83 Value {
84 amount: lambda_2_i,
85 asset_id: self.trading_pair.asset_2(),
86 },
87 output_2_rseed,
88 )
89 .expect("claim address is valid");
90
91 (output_1_note, output_2_note)
92 }
93
94 pub fn swap_commitment(&self) -> StateCommitment {
98 let inner = hash_7(
99 &DOMAIN_SEPARATOR,
100 (
101 Fq::from_le_bytes_mod_order(&self.rseed.to_bytes()[..]),
102 self.claim_fee.0.amount.into(),
103 self.claim_fee.0.asset_id.0,
104 self.claim_address
105 .diversified_generator()
106 .vartime_compress_to_field(),
107 *self.claim_address.transmission_key_s(),
108 Fq::from_le_bytes_mod_order(&self.claim_address.clue_key().0[..]),
109 hash_4(
110 &DOMAIN_SEPARATOR,
111 (
112 self.trading_pair.asset_1().0,
113 self.trading_pair.asset_2().0,
114 self.delta_1_i.into(),
115 self.delta_2_i.into(),
116 ),
117 ),
118 ),
119 );
120
121 StateCommitment(inner)
122 }
123
124 pub fn diversified_generator(&self) -> &decaf377::Element {
125 self.claim_address.diversified_generator()
126 }
127
128 pub fn transmission_key(&self) -> &ka::Public {
129 self.claim_address.transmission_key()
130 }
131
132 pub fn encrypt(&self, ovk: &OutgoingViewingKey) -> SwapPayload {
133 let commitment = self.swap_commitment();
134 let key = PayloadKey::derive_swap(ovk, commitment);
135 let swap_plaintext: [u8; SWAP_LEN_BYTES] = self.into();
136 let encryption_result = key.encrypt_swap(swap_plaintext.to_vec());
137
138 let ciphertext: [u8; SWAP_CIPHERTEXT_BYTES] = encryption_result
139 .try_into()
140 .expect("swap encryption result fits in ciphertext len");
141
142 SwapPayload {
143 encrypted_swap: SwapCiphertext(ciphertext),
144 commitment,
145 }
146 }
147
148 pub fn delta_1_value(&self) -> Value {
149 Value {
150 amount: self.delta_1_i,
151 asset_id: self.trading_pair.asset_1,
152 }
153 }
154
155 pub fn delta_2_value(&self) -> Value {
156 Value {
157 amount: self.delta_2_i,
158 asset_id: self.trading_pair.asset_2,
159 }
160 }
161
162 pub fn new<R: RngCore + CryptoRng>(
163 rng: &mut R,
164 trading_pair: TradingPair,
165 delta_1_i: Amount,
166 delta_2_i: Amount,
167 claim_fee: Fee,
168 claim_address: Address,
169 ) -> SwapPlaintext {
170 let rseed = Rseed::generate(rng);
171
172 Self {
173 trading_pair,
174 delta_1_i,
175 delta_2_i,
176 claim_fee,
177 claim_address,
178 rseed,
179 }
180 }
181}
182
183pub struct SwapPlaintextVar {
184 pub claim_fee: ValueVar,
185 pub delta_1_i: AmountVar,
186 pub trading_pair: TradingPairVar,
187 pub delta_2_i: AmountVar,
188 pub claim_address: AddressVar,
189 pub rseed: FqVar,
190}
191
192impl SwapPlaintextVar {
193 pub fn delta_1_value(&self) -> ValueVar {
194 ValueVar {
195 amount: self.delta_1_i.clone(),
196 asset_id: self.trading_pair.asset_1.clone(),
197 }
198 }
199
200 pub fn delta_2_value(&self) -> ValueVar {
201 ValueVar {
202 amount: self.delta_2_i.clone(),
203 asset_id: self.trading_pair.asset_2.clone(),
204 }
205 }
206
207 pub fn commit(&self) -> Result<StateCommitmentVar, SynthesisError> {
208 let cs = self.delta_1_i.amount.cs();
210
211 let domain_sep = FqVar::new_constant(cs.clone(), *DOMAIN_SEPARATOR)?;
212 let compressed_g_d = self
213 .claim_address
214 .diversified_generator()
215 .compress_to_field()?;
216
217 let inner_hash4 = poseidon377::r1cs::hash_4(
218 cs.clone(),
219 &domain_sep,
220 (
221 self.trading_pair.asset_1.asset_id.clone(),
222 self.trading_pair.asset_2.asset_id.clone(),
223 self.delta_1_i.amount.clone(),
224 self.delta_2_i.amount.clone(),
225 ),
226 )?;
227
228 let inner = poseidon377::r1cs::hash_7(
229 cs,
230 &domain_sep,
231 (
232 self.rseed.clone(),
233 self.claim_fee.amount.amount.clone(),
234 self.claim_fee.asset_id.asset_id.clone(),
235 compressed_g_d,
236 self.claim_address.transmission_key().compress_to_field()?,
237 self.claim_address.clue_key(),
238 inner_hash4,
239 ),
240 )?;
241
242 Ok(StateCommitmentVar { inner })
243 }
244}
245
246impl AllocVar<SwapPlaintext, Fq> for SwapPlaintextVar {
247 fn new_variable<T: std::borrow::Borrow<SwapPlaintext>>(
248 cs: impl Into<ark_relations::r1cs::Namespace<Fq>>,
249 f: impl FnOnce() -> Result<T, SynthesisError>,
250 mode: ark_r1cs_std::prelude::AllocationMode,
251 ) -> Result<Self, SynthesisError> {
252 let ns = cs.into();
253 let cs = ns.cs();
254 let swap_plaintext = f()?.borrow().clone();
255 let claim_fee =
256 ValueVar::new_variable(cs.clone(), || Ok(swap_plaintext.claim_fee.0), mode)?;
257 let delta_1_i = AmountVar::new_variable(cs.clone(), || Ok(swap_plaintext.delta_1_i), mode)?;
258
259 let trading_pair = TradingPairVar::new_variable_unchecked(
264 cs.clone(),
265 || Ok(swap_plaintext.trading_pair),
266 mode,
267 )?;
268 let delta_2_i = AmountVar::new_variable(cs.clone(), || Ok(swap_plaintext.delta_2_i), mode)?;
269 let claim_address =
270 AddressVar::new_variable(cs.clone(), || Ok(swap_plaintext.claim_address), mode)?;
271 let rseed = FqVar::new_variable(
272 cs,
273 || {
274 Ok(Fq::from_le_bytes_mod_order(
275 &swap_plaintext.rseed.to_bytes()[..],
276 ))
277 },
278 mode,
279 )?;
280 Ok(Self {
281 claim_fee,
282 delta_1_i,
283 trading_pair,
284 delta_2_i,
285 claim_address,
286 rseed,
287 })
288 }
289}
290
291impl DomainType for SwapPlaintext {
292 type Proto = pb::SwapPlaintext;
293}
294
295impl TryFrom<pb::SwapPlaintext> for SwapPlaintext {
296 type Error = anyhow::Error;
297 fn try_from(plaintext: pb::SwapPlaintext) -> anyhow::Result<Self> {
298 Ok(Self {
299 delta_1_i: plaintext
300 .delta_1_i
301 .ok_or_else(|| anyhow!("missing delta_1_i"))?
302 .try_into()?,
303 delta_2_i: plaintext
304 .delta_2_i
305 .ok_or_else(|| anyhow!("missing delta_2_i"))?
306 .try_into()?,
307 claim_address: plaintext
308 .claim_address
309 .ok_or_else(|| anyhow::anyhow!("missing SwapPlaintext claim address"))?
310 .try_into()
311 .map_err(|_| anyhow::anyhow!("invalid claim address in SwapPlaintext"))?,
312 claim_fee: plaintext
313 .claim_fee
314 .ok_or_else(|| anyhow::anyhow!("missing SwapPlaintext claim_fee"))?
315 .try_into()?,
316 trading_pair: plaintext
317 .trading_pair
318 .ok_or_else(|| anyhow::anyhow!("missing trading pair in SwapPlaintext"))?
319 .try_into()?,
320 rseed: Rseed(plaintext.rseed.as_slice().try_into()?),
321 })
322 }
323}
324
325impl From<SwapPlaintext> for pb::SwapPlaintext {
326 fn from(plaintext: SwapPlaintext) -> Self {
327 Self {
328 delta_1_i: Some(plaintext.delta_1_i.into()),
329 delta_2_i: Some(plaintext.delta_2_i.into()),
330 claim_fee: Some(plaintext.claim_fee.into()),
331 claim_address: Some(plaintext.claim_address.into()),
332 trading_pair: Some(plaintext.trading_pair.into()),
333 rseed: plaintext.rseed.to_bytes().to_vec(),
334 }
335 }
336}
337
338impl From<&SwapPlaintext> for [u8; SWAP_LEN_BYTES] {
339 fn from(swap: &SwapPlaintext) -> [u8; SWAP_LEN_BYTES] {
340 let mut bytes = [0u8; SWAP_LEN_BYTES];
341 bytes[0..64].copy_from_slice(&swap.trading_pair.to_bytes());
342 bytes[64..80].copy_from_slice(&swap.delta_1_i.to_le_bytes());
343 bytes[80..96].copy_from_slice(&swap.delta_2_i.to_le_bytes());
344 bytes[96..112].copy_from_slice(&swap.claim_fee.0.amount.to_le_bytes());
345 bytes[112..144].copy_from_slice(&swap.claim_fee.0.asset_id.to_bytes());
346 let pb_address = pb_keys::Address::from(swap.claim_address.clone());
347 bytes[144..224].copy_from_slice(&pb_address.inner);
348 bytes[224..256].copy_from_slice(&swap.rseed.to_bytes());
349 bytes
350 }
351}
352
353impl From<SwapPlaintext> for [u8; SWAP_LEN_BYTES] {
354 fn from(swap: SwapPlaintext) -> [u8; SWAP_LEN_BYTES] {
355 (&swap).into()
356 }
357}
358
359impl TryFrom<&[u8]> for SwapPlaintext {
360 type Error = Error;
361
362 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
363 if bytes.len() != SWAP_LEN_BYTES {
364 anyhow::bail!("incorrect length for serialized swap plaintext");
365 }
366
367 let tp_bytes: [u8; 64] = bytes[0..64]
368 .try_into()
369 .map_err(|_| anyhow!("error fetching trading pair bytes"))?;
370 let delta_1_bytes: [u8; 16] = bytes[64..80]
371 .try_into()
372 .map_err(|_| anyhow!("error fetching delta1 bytes"))?;
373 let delta_2_bytes: [u8; 16] = bytes[80..96]
374 .try_into()
375 .map_err(|_| anyhow!("error fetching delta2 bytes"))?;
376 let fee_amount_bytes: [u8; 16] = bytes[96..112]
377 .try_into()
378 .map_err(|_| anyhow!("error fetching fee amount bytes"))?;
379 let fee_asset_id_bytes: [u8; 32] = bytes[112..144]
380 .try_into()
381 .map_err(|_| anyhow!("error fetching fee asset ID bytes"))?;
382 let address_bytes: [u8; 80] = bytes[144..224]
383 .try_into()
384 .map_err(|_| anyhow!("error fetching address bytes"))?;
385 let pb_address = pb_keys::Address {
386 inner: address_bytes.to_vec(),
387 alt_bech32m: String::new(),
388 };
389 let rseed: [u8; 32] = bytes[224..256]
390 .try_into()
391 .map_err(|_| anyhow!("error fetching rseed bytes"))?;
392
393 Ok(SwapPlaintext {
394 trading_pair: tp_bytes
395 .try_into()
396 .map_err(|_| anyhow!("error deserializing trading pair"))?,
397 delta_1_i: Amount::from_le_bytes(delta_1_bytes),
398 delta_2_i: Amount::from_le_bytes(delta_2_bytes),
399 claim_fee: Fee(Value {
400 amount: Amount::from_le_bytes(fee_amount_bytes),
401 asset_id: asset::Id::try_from(fee_asset_id_bytes)?,
402 }),
403 claim_address: pb_address.try_into()?,
404 rseed: Rseed(rseed),
405 })
406 }
407}
408
409impl TryFrom<[u8; SWAP_LEN_BYTES]> for SwapPlaintext {
410 type Error = Error;
411
412 fn try_from(bytes: [u8; SWAP_LEN_BYTES]) -> Result<SwapPlaintext, Self::Error> {
413 (&bytes[..]).try_into()
414 }
415}
416
417#[cfg(test)]
418mod tests {
419
420 use rand_core::OsRng;
421
422 use super::*;
423 use penumbra_sdk_asset::{asset, Value};
424 use penumbra_sdk_keys::keys::{Bip44Path, SeedPhrase, SpendKey};
425
426 #[test]
427 fn swap_encryption_and_decryption() {
429 let mut rng = OsRng;
430
431 let seed_phrase = SeedPhrase::generate(rng);
432 let sk = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
433 let fvk = sk.full_viewing_key();
434 let ivk = fvk.incoming();
435 let ovk = fvk.outgoing();
436 let (dest, _dtk_d) = ivk.payment_address(0u32.into());
437 let trading_pair = TradingPair::new(
438 asset::Cache::with_known_assets()
439 .get_unit("upenumbra")
440 .unwrap()
441 .id(),
442 asset::Cache::with_known_assets()
443 .get_unit("nala")
444 .unwrap()
445 .id(),
446 );
447
448 let swap = SwapPlaintext::new(
449 &mut rng,
450 trading_pair,
451 100000u64.into(),
452 1u64.into(),
453 Fee(Value {
454 amount: 3u64.into(),
455 asset_id: asset::Cache::with_known_assets()
456 .get_unit("upenumbra")
457 .unwrap()
458 .id(),
459 }),
460 dest,
461 );
462
463 let ciphertext = swap.encrypt(ovk).encrypted_swap;
464 let plaintext = SwapCiphertext::decrypt(&ciphertext, ovk, swap.swap_commitment())
465 .expect("can decrypt swap");
466
467 assert_eq!(plaintext, swap);
468 }
469}