penumbra_sdk_dex/swap/
payload.rs

1use anyhow::anyhow;
2use penumbra_sdk_keys::FullViewingKey;
3use penumbra_sdk_proto::penumbra::core::component::dex::v1 as pb;
4use serde::{Deserialize, Serialize};
5
6use super::{SwapCiphertext, SwapPlaintext};
7
8#[derive(Clone, Debug, Serialize, Deserialize)]
9#[serde(try_from = "pb::SwapPayload", into = "pb::SwapPayload")]
10pub struct SwapPayload {
11    pub commitment: penumbra_sdk_tct::StateCommitment,
12    pub encrypted_swap: SwapCiphertext,
13}
14
15impl SwapPayload {
16    pub fn trial_decrypt(&self, fvk: &FullViewingKey) -> Option<SwapPlaintext> {
17        // Try to decrypt the swap ciphertext. If it doesn't decrypt, it wasn't meant for us.
18        let swap = self
19            .encrypted_swap
20            .decrypt(fvk.outgoing(), self.commitment)
21            .ok()?;
22        tracing::debug!(swap_commitment = ?self.commitment, ?swap, "found swap while scanning");
23
24        // Before returning, though, we want to perform integrity checks on the
25        // swap plaintext, since it could have been sent by unseen overlords of
26        // endless deceptive power. One can never be too careful.
27        //
28        // As in trial_decrypt for notes, we don't want to return errors, to
29        // avoid the possibility of "REJECT" style attacks.
30
31        // Check that the swap plaintext matches the swap commitment.
32        if swap.swap_commitment() != self.commitment {
33            // This should be a warning, because no honestly generated swap plaintext should
34            // fail to match the swap commitment actually included in the chain.
35            tracing::warn!("decrypted swap does not match provided swap commitment");
36            return None;
37        }
38
39        // Check that the swap outputs are spendable by this fvk's spending key.
40        if !fvk.incoming().views_address(&swap.claim_address) {
41            // This should be a warning, because no honestly generated swap plaintext should
42            // mismatch the FVK that can detect and decrypt it.
43            tracing::warn!("decrypted swap that is not claimable by provided full viewing key");
44            return None;
45        }
46
47        Some(swap)
48    }
49}
50
51impl From<SwapPayload> for pb::SwapPayload {
52    fn from(msg: SwapPayload) -> Self {
53        pb::SwapPayload {
54            commitment: Some(msg.commitment.into()),
55            encrypted_swap: msg.encrypted_swap.0.to_vec(),
56        }
57    }
58}
59
60impl TryFrom<pb::SwapPayload> for SwapPayload {
61    type Error = anyhow::Error;
62
63    fn try_from(msg: pb::SwapPayload) -> Result<Self, Self::Error> {
64        let commitment = msg
65            .commitment
66            .ok_or_else(|| anyhow!("missing commitment"))?
67            .try_into()?;
68        let encrypted_swap = SwapCiphertext(
69            msg.encrypted_swap
70                .try_into()
71                .map_err(|_| anyhow!("expected correct length swap ciphertext"))?,
72        );
73        Ok(Self {
74            commitment,
75            encrypted_swap,
76        })
77    }
78}