penumbra_sdk_shielded_pool/
note_payload.rs

1use anyhow::{Context, Error};
2
3use penumbra_sdk_keys::keys::FullViewingKey;
4use penumbra_sdk_num::Amount;
5use penumbra_sdk_proto::{penumbra::core::component::shielded_pool::v1 as pb, DomainType};
6use serde::{Deserialize, Serialize};
7
8use crate::{note, Note, NoteCiphertext};
9use decaf377_ka as ka;
10
11#[derive(Clone, Serialize, Deserialize)]
12#[serde(try_from = "pb::NotePayload", into = "pb::NotePayload")]
13pub struct NotePayload {
14    pub note_commitment: note::StateCommitment,
15    pub ephemeral_key: ka::Public,
16    pub encrypted_note: NoteCiphertext,
17}
18
19impl NotePayload {
20    pub fn trial_decrypt(&self, fvk: &FullViewingKey) -> Option<Note> {
21        // Try to decrypt the encrypted note using the ephemeral key and persistent incoming
22        // viewing key -- if it doesn't decrypt, it wasn't meant for us.
23        let note = Note::decrypt(&self.encrypted_note, fvk.incoming(), &self.ephemeral_key).ok()?;
24        tracing::debug!(note_commitment = ?note.commit(), ?note, "found note while scanning");
25
26        // Verification logic (if any fails, return None & log error)
27        // Reject notes with zero amount
28        if note.amount() == Amount::zero() {
29            // This is only debug-level because it can happen honestly (e.g., swap claims, dummy spends).
30            tracing::debug!("ignoring note recording zero assets");
31            return None;
32        }
33        // Make sure spendable by keys
34        if !note.controlled_by(fvk) {
35            // This should be a warning, because no honestly generated note plaintext should
36            // mismatch the FVK that can detect and decrypt it.
37            tracing::warn!("decrypted note that is not spendable by provided full viewing key");
38            return None;
39        }
40        // Make sure note commitment matches
41        if note.commit() != self.note_commitment {
42            // This should be a warning, because no honestly generated note plaintext should
43            // fail to match the note commitment actually included in the chain.
44            tracing::warn!("decrypted note does not match provided note commitment");
45            return None;
46        }
47
48        // NOTE: We intentionally return `Option` here instead of `Result`
49        // such that we gracefully drop malformed notes instead of returning an error
50        // that may propagate up the call stack and cause a panic.
51        // All errors in parsing notes must not cause a panic in the view service.
52        // A panic when parsing a specific note could link the fact that the malformed
53        // note can be successfully decrypted with a specific IP.
54        //
55        // See "REJECT" attack (CVE-2019-16930) for a similar attack in ZCash
56        // Section 4.1 in https://crypto.stanford.edu/timings/pingreject.pdf
57        Some(note)
58    }
59}
60
61impl std::fmt::Debug for NotePayload {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.debug_struct("NotePayload")
64            .field("note_commitment", &self.note_commitment)
65            .field("ephemeral_key", &self.ephemeral_key)
66            .field("encrypted_note", &"...")
67            .finish()
68    }
69}
70
71impl DomainType for NotePayload {
72    type Proto = pb::NotePayload;
73}
74
75impl From<NotePayload> for pb::NotePayload {
76    fn from(msg: NotePayload) -> Self {
77        pb::NotePayload {
78            note_commitment: Some(msg.note_commitment.into()),
79            ephemeral_key: msg.ephemeral_key.0.to_vec(),
80            encrypted_note: Some(msg.encrypted_note.into()),
81        }
82    }
83}
84
85impl TryFrom<pb::NotePayload> for NotePayload {
86    type Error = Error;
87
88    fn try_from(proto: pb::NotePayload) -> anyhow::Result<Self, Self::Error> {
89        Ok(NotePayload {
90            note_commitment: proto
91                .note_commitment
92                .ok_or_else(|| anyhow::anyhow!("missing note commitment"))?
93                .try_into()?,
94            ephemeral_key: ka::Public::try_from(&proto.ephemeral_key[..])
95                .context("ephemeral key malformed")?,
96            encrypted_note: proto
97                .encrypted_note
98                .ok_or_else(|| anyhow::anyhow!("missing encrypted note"))?
99                .try_into()?,
100        })
101    }
102}