Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UIP-4 Spend Backreferences #4922

Merged
merged 13 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/bin/pcli/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use penumbra_keys::{
};
use penumbra_proof_params::GROTH16_PROOF_LENGTH_BYTES;
use penumbra_sct::Nullifier;
use penumbra_shielded_pool::{Note, NoteView};
use penumbra_shielded_pool::{EncryptedBackref, Note, NoteView};
use penumbra_tct::structure::Hash;
use penumbra_transaction::{view, ActionPlan, ActionView, TransactionPlan, TransactionView};
use termion::{color, input::TermRead};
Expand Down Expand Up @@ -82,6 +82,8 @@ fn pretty_print_transaction_plan(
balance_commitment: dummy_commitment(),
nullifier: Nullifier(Fq::default()),
rk: dummy_pk(),
encrypted_backref: EncryptedBackref::try_from([0u8; 0])
.expect("can create dummy encrypted backref"),
},
auth_sig: dummy_sig(),
proof: dummy_proof_spend(),
Expand Down
221 changes: 221 additions & 0 deletions crates/core/component/shielded-pool/src/backref.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use anyhow::Result;
use chacha20poly1305::{
aead::{Aead, NewAead},
ChaCha20Poly1305, Nonce,
};

use penumbra_keys::BackreferenceKey;
use penumbra_sct::Nullifier;
use penumbra_tct as tct;

pub const ENCRYPTED_BACKREF_LEN: usize = 48;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Backref {
note_commitment: tct::StateCommitment,
}

#[derive(Clone, Debug)]
pub struct EncryptedBackref {
/// The inner bytes can either have 0 or `ENCRYPTED_BACKREF_LEN` bytes.
bytes: Vec<u8>,
}

impl Backref {
pub fn new(note_commitment: tct::StateCommitment) -> Self {
Self { note_commitment }
}

pub fn encrypt(
&self,
brk: &BackreferenceKey,
nullifier: &Nullifier,
) -> Result<EncryptedBackref> {
let cipher = ChaCha20Poly1305::new(&brk.0);

// Nonce is the first 12 bytes of the nullifier
let nonce_bytes = &nullifier.to_bytes()[..12];
let nonce = Nonce::from_slice(&nonce_bytes);

let plaintext = self.note_commitment.0.to_bytes();

let ciphertext = cipher
.encrypt(nonce, plaintext.as_ref())
redshiftzero marked this conversation as resolved.
Show resolved Hide resolved
.map_err(|_| anyhow::anyhow!("encryption error"))?;

Ok(EncryptedBackref { bytes: ciphertext })
}
}

impl EncryptedBackref {
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}

pub fn len(&self) -> usize {
self.bytes.len()
}

pub fn dummy() -> Self {
Self { bytes: vec![] }
}

/// Decrypts the encrypted backref, returning a backref if the decryption is successful,
/// or `None` if the encrypted backref is zero-length.
pub fn decrypt(
&self,
brk: &BackreferenceKey,
nullifier: &Nullifier,
) -> Result<Option<Backref>> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having Result is smart here, because then you can distinguish between having not put in a back reference, in which case maybe the reference would have pointed to a spend of yours, and a reference you know doesn't belong to you because you can't decrypt it.

// We might have a 0-length encrypted backref, which
// is treated as a valid value and means that the note has no backref.
if self.is_empty() {
return Ok(None);
}

let cipher = ChaCha20Poly1305::new(&brk.0);

let nonce_bytes = &nullifier.to_bytes()[..12];
let nonce = Nonce::from_slice(&nonce_bytes);

let plaintext = cipher
.decrypt(nonce, self.bytes.as_ref())
.map_err(|_| anyhow::anyhow!("decryption error"))?;

let note_commitment_bytes: [u8; 32] = plaintext
.try_into()
.map_err(|_| anyhow::anyhow!("decryption error"))?;

let backref = Backref::try_from(note_commitment_bytes)
.map_err(|_| anyhow::anyhow!("decryption error"))?;

Ok(Some(backref))
}
}

impl TryFrom<[u8; 32]> for Backref {
type Error = anyhow::Error;

fn try_from(bytes: [u8; 32]) -> Result<Self> {
Ok(Self {
note_commitment: tct::StateCommitment::try_from(bytes)
.map_err(|_| anyhow::anyhow!("invalid note commitment"))?,
})
}
}

// EncryptedBackrefs can either have 0 or ENCRYPTED_BACKREF_LEN bytes.

impl TryFrom<[u8; ENCRYPTED_BACKREF_LEN]> for EncryptedBackref {
type Error = anyhow::Error;

fn try_from(bytes: [u8; ENCRYPTED_BACKREF_LEN]) -> Result<Self> {
Ok(Self {
bytes: bytes.to_vec(),
})
}
}

impl TryFrom<[u8; 0]> for EncryptedBackref {
type Error = anyhow::Error;

fn try_from(bytes: [u8; 0]) -> Result<Self> {
Ok(Self {
bytes: bytes.to_vec(),
})
}
}

impl From<EncryptedBackref> for Vec<u8> {
fn from(encrypted_backref: EncryptedBackref) -> Vec<u8> {
encrypted_backref.bytes
}
}

#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;

use penumbra_asset::{asset, Value};
use penumbra_keys::keys::{Bip44Path, SeedPhrase, SpendKey};

use crate::{Note, Rseed};

proptest! {
#[test]
fn encrypted_backref_zero_length(seed_phrase_randomness in any::<[u8; 32]>(), amount_to_send in any::<u64>(), rseed_randomness in any::<[u8; 32]>()) {
let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
let sk = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
let fvk = sk.full_viewing_key();
let brk = fvk.backref_key();

let ivk = fvk.incoming();
let (sender, _dtk_d) = ivk.payment_address(0u32.into());

let value_to_send = Value {
amount: amount_to_send.into(),
asset_id: asset::Cache::with_known_assets()
.get_unit("upenumbra")
.unwrap()
.id(),
};
let rseed = Rseed(rseed_randomness);

let note = Note::from_parts(sender, value_to_send, rseed).expect("valid note");
let note_commitment: penumbra_tct::StateCommitment = note.commit();
let nk = *sk.nullifier_key();
let mut sct = tct::Tree::new();

sct.insert(tct::Witness::Keep, note_commitment).unwrap();
let state_commitment_proof = sct.witness(note_commitment).unwrap();
let nullifier = Nullifier::derive(&nk, state_commitment_proof.position(), &note_commitment);

let encrypted_backref = EncryptedBackref::dummy();
assert!(encrypted_backref.is_empty());
assert_eq!(encrypted_backref.len(), 0);

// Decrypting a zero-length encrypted backref should return `None`.
let decrypted_backref = encrypted_backref.decrypt(&brk, &nullifier).unwrap();
assert_eq!(decrypted_backref, None);
}
}

proptest! {
#[test]
fn encrypted_backref_round_trip(seed_phrase_randomness in any::<[u8; 32]>(), amount_to_send in any::<u64>(), rseed_randomness in any::<[u8; 32]>()) {
let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
let sk = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
let fvk = sk.full_viewing_key();
let brk = fvk.backref_key();

let ivk = fvk.incoming();
let (sender, _dtk_d) = ivk.payment_address(0u32.into());

let value_to_send = Value {
amount: amount_to_send.into(),
asset_id: asset::Cache::with_known_assets()
.get_unit("upenumbra")
.unwrap()
.id(),
};
let rseed = Rseed(rseed_randomness);

let note = Note::from_parts(sender, value_to_send, rseed).expect("valid note");
let note_commitment: penumbra_tct::StateCommitment = note.commit();
let nk = *sk.nullifier_key();
let mut sct = tct::Tree::new();

sct.insert(tct::Witness::Keep, note_commitment).unwrap();
let state_commitment_proof = sct.witness(note_commitment).unwrap();
let nullifier = Nullifier::derive(&nk, state_commitment_proof.position(), &note_commitment);

let backref = Backref::new(note_commitment);
let encrypted_backref = backref.encrypt(&brk, &nullifier).unwrap();

let decrypted_backref = encrypted_backref.decrypt(&brk, &nullifier).unwrap();

assert_eq!(Some(backref), decrypted_backref);
}
}
}
3 changes: 3 additions & 0 deletions crates/core/component/shielded-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pub mod nullifier_derivation;
pub mod output;
pub mod spend;

pub mod backref;
pub use backref::{Backref, EncryptedBackref};

pub use convert::{ConvertCircuit, ConvertProof, ConvertProofPrivate, ConvertProofPublic};
pub use nullifier_derivation::{
NullifierDerivationCircuit, NullifierDerivationProof, NullifierDerivationProofPrivate,
Expand Down
20 changes: 20 additions & 0 deletions crates/core/component/shielded-pool/src/spend/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use penumbra_txhash::{EffectHash, EffectingData};
use serde::{Deserialize, Serialize};

use crate::SpendProof;
use crate::{backref::ENCRYPTED_BACKREF_LEN, EncryptedBackref};

#[derive(Clone, Debug)]
pub struct Spend {
Expand All @@ -23,6 +24,7 @@ pub struct Body {
pub balance_commitment: balance::Commitment,
pub nullifier: Nullifier,
pub rk: VerificationKey<SpendAuth>,
pub encrypted_backref: EncryptedBackref,
}

impl EffectingData for Body {
Expand Down Expand Up @@ -91,6 +93,7 @@ impl From<Body> for pb::SpendBody {
balance_commitment: Some(msg.balance_commitment.into()),
nullifier: Some(msg.nullifier.into()),
rk: Some(msg.rk.into()),
encrypted_backref: msg.encrypted_backref.into(),
}
}
}
Expand All @@ -117,10 +120,27 @@ impl TryFrom<pb::SpendBody> for Body {
.try_into()
.context("malformed rk")?;

// `EncryptedBackref` must have 0 or `ENCRYPTED_BACKREF_LEN` bytes.
let encrypted_backref: EncryptedBackref;
if proto.encrypted_backref.len() == ENCRYPTED_BACKREF_LEN {
let bytes: [u8; ENCRYPTED_BACKREF_LEN] = proto
.encrypted_backref
.try_into()
.map_err(|_| anyhow::anyhow!("invalid encrypted backref"))?;
encrypted_backref = EncryptedBackref::try_from(bytes)
.map_err(|_| anyhow::anyhow!("invalid encrypted backref"))?;
} else if proto.encrypted_backref.len() == 0 {
encrypted_backref = EncryptedBackref::try_from([0u8; ENCRYPTED_BACKREF_LEN])
redshiftzero marked this conversation as resolved.
Show resolved Hide resolved
.context("invalid encrypted backref")?;
} else {
return Err(anyhow::anyhow!("invalid encrypted backref length"));
}

Ok(Body {
balance_commitment,
nullifier,
rk,
encrypted_backref,
})
}
}
9 changes: 8 additions & 1 deletion crates/core/component/shielded-pool/src/spend/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use rand_core::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};

use super::{Body, Spend, SpendProof};
use crate::{Note, Rseed, SpendProofPrivate, SpendProofPublic};
use crate::{Backref, Note, Rseed, SpendProofPrivate, SpendProofPublic};

/// A planned [`Spend`](Spend).
#[derive(Clone, Debug, Deserialize, Serialize)]
Expand Down Expand Up @@ -75,10 +75,17 @@ impl SpendPlan {

/// Construct the [`spend::Body`] described by this [`SpendPlan`].
pub fn spend_body(&self, fvk: &FullViewingKey) -> Body {
// Construct the backreference for this spend.
let backref = Backref::new(self.note.commit());
// TODO: This is fallible
let encrypted_backref = backref
.encrypt(&fvk.backref_key(), &self.nullifier(fvk))
.expect("can encrypt");
Body {
balance_commitment: self.balance().commit(self.value_blinding),
nullifier: self.nullifier(fvk),
rk: self.rk(fvk),
encrypted_backref,
}
}

Expand Down
7 changes: 6 additions & 1 deletion crates/core/keys/src/keys/fvk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::keys::wallet_id::WalletId;
use crate::{
fmd, ka, prf,
rdsa::{SpendAuth, VerificationKey},
Address, AddressView,
Address, AddressView, BackreferenceKey,
};

use super::{AddressIndex, DiversifierKey, IncomingViewingKey, NullifierKey, OutgoingViewingKey};
Expand Down Expand Up @@ -120,6 +120,11 @@ impl FullViewingKey {
&self.ak
}

/// Construct the backreference key for this full viewing key.
pub fn backref_key(&self) -> BackreferenceKey {
BackreferenceKey::derive(self.outgoing()).clone()
}

/// Hashes the full viewing key into an [`WalletId`].
pub fn wallet_id(&self) -> WalletId {
let hash_result = hash_2(
Expand Down
2 changes: 1 addition & 1 deletion crates/core/keys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub mod test_keys;

pub use address::{Address, AddressVar, AddressView};
pub use keys::FullViewingKey;
pub use symmetric::PayloadKey;
pub use symmetric::{BackreferenceKey, PayloadKey};

fn fmt_hex<T: AsRef<[u8]>>(data: T, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", hex::encode(data))
Expand Down
17 changes: 17 additions & 0 deletions crates/core/keys/src/symmetric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,20 @@ impl TryFrom<&[u8]> for WrappedMemoKey {
Ok(Self(bytes))
}
}

/// Represents a symmetric `ChaCha20Poly1305` key used for Spend backreferences.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct BackreferenceKey(pub Key);

impl BackreferenceKey {
pub fn derive(ovk: &OutgoingViewingKey) -> Self {
let mut kdf_params = blake2b_simd::Params::new();
kdf_params.personal(b"Penumbra_Backref");
kdf_params.hash_length(32);
let mut kdf = kdf_params.to_state();
kdf.update(&ovk.to_bytes());

let key = kdf.finalize();
Self(*Key::from_slice(key.as_bytes()))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,9 @@ pub struct SpendBody {
pub rk: ::core::option::Option<
super::super::super::super::crypto::decaf377_rdsa::v1::SpendVerificationKey,
>,
/// An encryption of the commitment of the input note to the sender's OVK.
#[prost(bytes = "vec", tag = "7")]
pub encrypted_backref: ::prost::alloc::vec::Vec<u8>,
}
impl ::prost::Name for SpendBody {
const NAME: &'static str = "SpendBody";
Expand Down
Loading
Loading