diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 76436ebaac..345b75ab60 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -9,3 +9,4 @@ | 7 (Testnet 78) | v0.78.x | v0.37.5 | v1 | | 7 (Mainnet) | v0.79.x | v0.37.x | v1 | | 8 (Mainnet) | v0.80.x | v0.37.x | v1 | +| 9 (Mainnet) | v0.81.x | v0.37.x | v1 | diff --git a/crates/bin/pcli/src/command/tx.rs b/crates/bin/pcli/src/command/tx.rs index f1ea71713a..5b5629d23d 100644 --- a/crates/bin/pcli/src/command/tx.rs +++ b/crates/bin/pcli/src/command/tx.rs @@ -1140,6 +1140,7 @@ impl TxCmd { source_channel: ChannelId::from_str(format!("channel-{}", channel).as_ref())?, use_compat_address: *use_compat_address, use_transparent_address: *use_transparent_address, + ics20_memo: "".to_string(), }; let plan = Planner::new(OsRng) diff --git a/crates/bin/pcli/src/terminal.rs b/crates/bin/pcli/src/terminal.rs index a13f03d65b..95bf433242 100644 --- a/crates/bin/pcli/src/terminal.rs +++ b/crates/bin/pcli/src/terminal.rs @@ -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}; @@ -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(), diff --git a/crates/bin/pd/src/main.rs b/crates/bin/pd/src/main.rs index 35815c7d5a..59bca2cdbc 100644 --- a/crates/bin/pd/src/main.rs +++ b/crates/bin/pd/src/main.rs @@ -12,7 +12,7 @@ use cnidarium::Storage; use metrics_exporter_prometheus::PrometheusBuilder; use pd::{ cli::{NetworkCommand, Opt, RootCommand}, - migrate::Migration::{Mainnet1, ReadyToStart}, + migrate::Migration::{Mainnet2, ReadyToStart}, network::{ config::{get_network_dir, parse_tm_address, url_has_necessary_parts}, generate::NetworkConfig, @@ -473,7 +473,7 @@ async fn main() -> anyhow::Result<()> { let genesis_start = pd::migrate::last_block_timestamp(pd_home.clone()).await?; tracing::info!(?genesis_start, "last block timestamp"); - Mainnet1 + Mainnet2 .migrate(pd_home.clone(), comet_home, Some(genesis_start), force) .instrument(pd_migrate_span) .await diff --git a/crates/bin/pd/src/migrate.rs b/crates/bin/pd/src/migrate.rs index ad6e612a9e..2beaaed7e3 100644 --- a/crates/bin/pd/src/migrate.rs +++ b/crates/bin/pd/src/migrate.rs @@ -5,6 +5,7 @@ //! This module declares how local `pd` state should be altered, if at all, //! in order to be compatible with the network post-chain-upgrade. mod mainnet1; +mod mainnet2; mod reset_halt_bit; mod simple; mod testnet72; @@ -56,6 +57,9 @@ pub enum Migration { /// Mainnet-1 migration: /// - Restore IBC packet commitments for improperly handled withdrawal attempts Mainnet1, + /// Mainnet-2 migration: + /// - no-op + Mainnet2, } impl Migration { @@ -94,6 +98,9 @@ impl Migration { Migration::Mainnet1 => { mainnet1::migrate(storage, pd_home.clone(), genesis_start).await?; } + Migration::Mainnet2 => { + mainnet2::migrate(storage, pd_home.clone(), genesis_start).await?; + } // We keep historical migrations around for now, this will help inform an abstracted // design. Feel free to remove it if it's causing you trouble. _ => unimplemented!("the specified migration is unimplemented"), diff --git a/crates/bin/pd/src/migrate/mainnet2.rs b/crates/bin/pd/src/migrate/mainnet2.rs new file mode 100644 index 0000000000..32a4c4d364 --- /dev/null +++ b/crates/bin/pd/src/migrate/mainnet2.rs @@ -0,0 +1,103 @@ +//! Migration for shipping consensus-breaking IBC changes, fixing +//! how withdrawals from Penumbra to Noble are handled, and ensures that IBC +//! error messages from counterparty chains are processed. +use cnidarium::{StateDelta, Storage}; +use jmt::RootHash; +use penumbra_app::app::StateReadExt as _; +use penumbra_app::app_version::migrate_app_version; +use penumbra_governance::StateWriteExt; +use penumbra_sct::component::clock::EpochManager; +use penumbra_sct::component::clock::EpochRead; +use std::path::PathBuf; +use tracing::instrument; + +use crate::network::generate::NetworkConfig; + +/// Run the full migration, emitting a new genesis event, representing historical state. +/// +/// This will have the effect of reinserting packets which had acknowledgements containing +/// errors, and erroneously removed from state, as if the acknowledgements had contained successes. +#[instrument] +pub async fn migrate( + storage: Storage, + pd_home: PathBuf, + genesis_start: Option, +) -> anyhow::Result<()> { + // Setup: + let initial_state = storage.latest_snapshot(); + let chain_id = initial_state.get_chain_id().await?; + let root_hash = initial_state + .root_hash() + .await + .expect("chain state has a root hash"); + // We obtain the pre-upgrade hash solely to log it as a result. + let pre_upgrade_root_hash: RootHash = root_hash.into(); + let pre_upgrade_height = initial_state + .get_block_height() + .await + .expect("chain state has a block height"); + let post_upgrade_height = pre_upgrade_height.wrapping_add(1); + + let mut delta = StateDelta::new(initial_state); + let (migration_duration, post_upgrade_root_hash) = { + let start_time = std::time::SystemTime::now(); + + migrate_app_version(&mut delta, 9).await?; + + // Reset the application height and halt flag. + delta.ready_to_start(); + delta.put_block_height(0u64); + + // Finally, commit the changes to the chain state. + let post_upgrade_root_hash = storage.commit_in_place(delta).await?; + tracing::info!(?post_upgrade_root_hash, "post-migration root hash"); + + ( + start_time.elapsed().expect("start is set"), + post_upgrade_root_hash, + ) + }; + storage.release().await; + + // The migration is complete, now we need to generate a genesis file. To do this, we need + // to lookup a validator view from the chain, and specify the post-upgrade app hash and + // initial height. + let app_state = penumbra_app::genesis::Content { + chain_id, + ..Default::default() + }; + let mut genesis = NetworkConfig::make_genesis(app_state.clone()).expect("can make genesis"); + genesis.app_hash = post_upgrade_root_hash + .0 + .to_vec() + .try_into() + .expect("infallible conversion"); + + genesis.initial_height = post_upgrade_height as i64; + genesis.genesis_time = genesis_start.unwrap_or_else(|| { + let now = tendermint::time::Time::now(); + tracing::info!(%now, "no genesis time provided, detecting a testing setup"); + now + }); + let checkpoint = post_upgrade_root_hash.0.to_vec(); + let genesis = NetworkConfig::make_checkpoint(genesis, Some(checkpoint)); + let genesis_json = serde_json::to_string(&genesis).expect("can serialize genesis"); + tracing::info!("genesis: {}", genesis_json); + let genesis_path = pd_home.join("genesis.json"); + std::fs::write(genesis_path, genesis_json).expect("can write genesis"); + + let validator_state_path = pd_home.join("priv_validator_state.json"); + let fresh_validator_state = crate::network::generate::NetworkValidator::initial_state(); + std::fs::write(validator_state_path, fresh_validator_state).expect("can write validator state"); + + tracing::info!( + pre_upgrade_height, + post_upgrade_height, + ?pre_upgrade_root_hash, + ?post_upgrade_root_hash, + duration = migration_duration.as_secs(), + "successful migration!" + ); + + Ok(()) +} diff --git a/crates/core/app/src/app_version.rs b/crates/core/app/src/app_version.rs index 276d64b471..e45acf17ab 100644 --- a/crates/core/app/src/app_version.rs +++ b/crates/core/app/src/app_version.rs @@ -1,6 +1,6 @@ /// Representation of the Penumbra application version. Notably, this is distinct /// from the crate version(s). This number should only ever be incremented. -pub const APP_VERSION: u64 = 8; +pub const APP_VERSION: u64 = 9; cfg_if::cfg_if! { if #[cfg(feature="component")] { diff --git a/crates/core/app/src/app_version/component.rs b/crates/core/app/src/app_version/component.rs index ff38395027..891ee61882 100644 --- a/crates/core/app/src/app_version/component.rs +++ b/crates/core/app/src/app_version/component.rs @@ -16,6 +16,7 @@ fn version_to_software_version(version: u64) -> &'static str { 6 => "v0.77.x", 7 => "v0.79.x", 8 => "v0.80.x", + 9 => "v0.81.x", _ => "unknown", } } diff --git a/crates/core/app/tests/common/ibc_tests/relayer.rs b/crates/core/app/tests/common/ibc_tests/relayer.rs index 5807adf508..4b76f6d0d3 100644 --- a/crates/core/app/tests/common/ibc_tests/relayer.rs +++ b/crates/core/app/tests/common/ibc_tests/relayer.rs @@ -1486,6 +1486,7 @@ impl MockRelayer { // Penumbra <-> Penumbra so false use_compat_address: false, use_transparent_address: false, + ics20_memo: "".to_string(), }; // There will need to be `Spend` and `Output` actions // within the transaction in order for it to balance diff --git a/crates/core/component/shielded-pool/src/backref.rs b/crates/core/component/shielded-pool/src/backref.rs new file mode 100644 index 0000000000..7eef7f4d96 --- /dev/null +++ b/crates/core/component/shielded-pool/src/backref.rs @@ -0,0 +1,217 @@ +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, +} + +impl Backref { + pub fn new(note_commitment: tct::StateCommitment) -> Self { + Self { note_commitment } + } + + pub fn encrypt(&self, brk: &BackreferenceKey, nullifier: &Nullifier) -> 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()) + .expect("encryption should succeed "); + + 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> { + // 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 { + 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 { + Ok(Self { + bytes: bytes.to_vec(), + }) + } +} + +impl TryFrom<[u8; 0]> for EncryptedBackref { + type Error = anyhow::Error; + + fn try_from(bytes: [u8; 0]) -> Result { + Ok(Self { + bytes: bytes.to_vec(), + }) + } +} + +impl From for Vec { + fn from(encrypted_backref: EncryptedBackref) -> Vec { + 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::(), 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(), ¬e_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::(), 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(), ¬e_commitment); + + let backref = Backref::new(note_commitment); + let encrypted_backref = backref.encrypt(&brk, &nullifier); + + let decrypted_backref = encrypted_backref.decrypt(&brk, &nullifier).unwrap(); + + assert_eq!(Some(backref), decrypted_backref); + } + } +} diff --git a/crates/core/component/shielded-pool/src/ics20_withdrawal.rs b/crates/core/component/shielded-pool/src/ics20_withdrawal.rs index 83ae6ac4b6..bd8cc78667 100644 --- a/crates/core/component/shielded-pool/src/ics20_withdrawal.rs +++ b/crates/core/component/shielded-pool/src/ics20_withdrawal.rs @@ -44,6 +44,10 @@ pub struct Ics20Withdrawal { // Whether to use a transparent address for the return address in the withdrawal. pub use_transparent_address: bool, + // Arbitrary string data to be included in the `memo` field + // of the ICS-20 FungibleTokenPacketData for this withdrawal. + // Commonly used for packet forwarding support, or other protocols that may support usage of the memo field. + pub ics20_memo: String, } #[cfg(feature = "component")] @@ -122,7 +126,11 @@ impl From for pb::Ics20Withdrawal { timeout_time: w.timeout_time, source_channel: w.source_channel.to_string(), use_compat_address: w.use_compat_address, +<<<<<<< HEAD use_transparent_address: w.use_transparent_address, +======= + ics20_memo: w.ics20_memo.to_string(), +>>>>>>> release/v0.81.x } } } @@ -154,7 +162,11 @@ impl TryFrom for Ics20Withdrawal { timeout_time: s.timeout_time, source_channel: ChannelId::from_str(&s.source_channel)?, use_compat_address: s.use_compat_address, +<<<<<<< HEAD use_transparent_address: s.use_transparent_address, +======= + ics20_memo: s.ics20_memo, +>>>>>>> release/v0.81.x }) } } @@ -176,7 +188,7 @@ impl From for pb::FungibleTokenPacketData { denom: w.denom.to_string(), receiver: w.destination_chain_address, sender: return_address, - memo: "".to_string(), + memo: w.ics20_memo, } } } diff --git a/crates/core/component/shielded-pool/src/lib.rs b/crates/core/component/shielded-pool/src/lib.rs index 84d112ed52..04109d614e 100644 --- a/crates/core/component/shielded-pool/src/lib.rs +++ b/crates/core/component/shielded-pool/src/lib.rs @@ -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, diff --git a/crates/core/component/shielded-pool/src/spend/action.rs b/crates/core/component/shielded-pool/src/spend/action.rs index 2a548a592a..83517d6748 100644 --- a/crates/core/component/shielded-pool/src/spend/action.rs +++ b/crates/core/component/shielded-pool/src/spend/action.rs @@ -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 { @@ -23,6 +24,7 @@ pub struct Body { pub balance_commitment: balance::Commitment, pub nullifier: Nullifier, pub rk: VerificationKey, + pub encrypted_backref: EncryptedBackref, } impl EffectingData for Body { @@ -91,6 +93,7 @@ impl From 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(), } } } @@ -117,10 +120,26 @@ impl TryFrom 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::dummy(); + } else { + return Err(anyhow::anyhow!("invalid encrypted backref length")); + } + Ok(Body { balance_commitment, nullifier, rk, + encrypted_backref, }) } } diff --git a/crates/core/component/shielded-pool/src/spend/plan.rs b/crates/core/component/shielded-pool/src/spend/plan.rs index a79fd41d99..1602badd0b 100644 --- a/crates/core/component/shielded-pool/src/spend/plan.rs +++ b/crates/core/component/shielded-pool/src/spend/plan.rs @@ -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)] @@ -75,10 +75,14 @@ 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()); + let encrypted_backref = backref.encrypt(&fvk.backref_key(), &self.nullifier(fvk)); Body { balance_commitment: self.balance().commit(self.value_blinding), nullifier: self.nullifier(fvk), rk: self.rk(fvk), + encrypted_backref, } } diff --git a/crates/core/keys/src/keys/fvk.rs b/crates/core/keys/src/keys/fvk.rs index e9a029578f..44239700bf 100644 --- a/crates/core/keys/src/keys/fvk.rs +++ b/crates/core/keys/src/keys/fvk.rs @@ -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}; @@ -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( diff --git a/crates/core/keys/src/lib.rs b/crates/core/keys/src/lib.rs index 81c8c8be7f..a880e51832 100644 --- a/crates/core/keys/src/lib.rs +++ b/crates/core/keys/src/lib.rs @@ -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>(data: T, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", hex::encode(data)) diff --git a/crates/core/keys/src/symmetric.rs b/crates/core/keys/src/symmetric.rs index 4e383d15c3..cc04585ed4 100644 --- a/crates/core/keys/src/symmetric.rs +++ b/crates/core/keys/src/symmetric.rs @@ -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())) + } +} diff --git a/crates/proto/src/gen/penumbra.core.component.ibc.v1.rs b/crates/proto/src/gen/penumbra.core.component.ibc.v1.rs index 9c96386ed8..995f53eef1 100644 --- a/crates/proto/src/gen/penumbra.core.component.ibc.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.ibc.v1.rs @@ -74,8 +74,13 @@ pub struct Ics20Withdrawal { #[deprecated] #[prost(bool, tag = "8")] pub use_compat_address: bool, + /// Arbitrary string data to be included in the `memo` field + /// of the ICS-20 FungibleTokenPacketData for this withdrawal. + /// Commonly used for packet forwarding support, or other protocols that may support usage of the memo field. + #[prost(string, tag = "9")] + pub ics20_memo: ::prost::alloc::string::String, /// Whether to use a transparent address (bech32, 32-byte) for the return address in the withdrawal. - #[prost(bool, tag = "9")] + #[prost(bool, tag = "10")] pub use_transparent_address: bool, } impl ::prost::Name for Ics20Withdrawal { diff --git a/crates/proto/src/gen/penumbra.core.component.ibc.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.ibc.v1.serde.rs index c7db081fa6..846d45e6c9 100644 --- a/crates/proto/src/gen/penumbra.core.component.ibc.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.ibc.v1.serde.rs @@ -1057,6 +1057,9 @@ impl serde::Serialize for Ics20Withdrawal { if self.use_compat_address { len += 1; } + if !self.ics20_memo.is_empty() { + len += 1; + } if self.use_transparent_address { len += 1; } @@ -1086,6 +1089,9 @@ impl serde::Serialize for Ics20Withdrawal { if self.use_compat_address { struct_ser.serialize_field("useCompatAddress", &self.use_compat_address)?; } + if !self.ics20_memo.is_empty() { + struct_ser.serialize_field("ics20Memo", &self.ics20_memo)?; + } if self.use_transparent_address { struct_ser.serialize_field("useTransparentAddress", &self.use_transparent_address)?; } @@ -1113,6 +1119,8 @@ impl<'de> serde::Deserialize<'de> for Ics20Withdrawal { "sourceChannel", "use_compat_address", "useCompatAddress", + "ics20_memo", + "ics20Memo", "use_transparent_address", "useTransparentAddress", ]; @@ -1127,6 +1135,7 @@ impl<'de> serde::Deserialize<'de> for Ics20Withdrawal { TimeoutTime, SourceChannel, UseCompatAddress, + Ics20Memo, UseTransparentAddress, __SkipField__, } @@ -1158,6 +1167,7 @@ impl<'de> serde::Deserialize<'de> for Ics20Withdrawal { "timeoutTime" | "timeout_time" => Ok(GeneratedField::TimeoutTime), "sourceChannel" | "source_channel" => Ok(GeneratedField::SourceChannel), "useCompatAddress" | "use_compat_address" => Ok(GeneratedField::UseCompatAddress), + "ics20Memo" | "ics20_memo" => Ok(GeneratedField::Ics20Memo), "useTransparentAddress" | "use_transparent_address" => Ok(GeneratedField::UseTransparentAddress), _ => Ok(GeneratedField::__SkipField__), } @@ -1186,6 +1196,7 @@ impl<'de> serde::Deserialize<'de> for Ics20Withdrawal { let mut timeout_time__ = None; let mut source_channel__ = None; let mut use_compat_address__ = None; + let mut ics20_memo__ = None; let mut use_transparent_address__ = None; while let Some(k) = map_.next_key()? { match k { @@ -1239,6 +1250,12 @@ impl<'de> serde::Deserialize<'de> for Ics20Withdrawal { } use_compat_address__ = Some(map_.next_value()?); } + GeneratedField::Ics20Memo => { + if ics20_memo__.is_some() { + return Err(serde::de::Error::duplicate_field("ics20Memo")); + } + ics20_memo__ = Some(map_.next_value()?); + } GeneratedField::UseTransparentAddress => { if use_transparent_address__.is_some() { return Err(serde::de::Error::duplicate_field("useTransparentAddress")); @@ -1259,6 +1276,7 @@ impl<'de> serde::Deserialize<'de> for Ics20Withdrawal { timeout_time: timeout_time__.unwrap_or_default(), source_channel: source_channel__.unwrap_or_default(), use_compat_address: use_compat_address__.unwrap_or_default(), + ics20_memo: ics20_memo__.unwrap_or_default(), use_transparent_address: use_transparent_address__.unwrap_or_default(), }) } diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs index 1362033de8..e287e3e5e6 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.rs @@ -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, } impl ::prost::Name for SpendBody { const NAME: &'static str = "SpendBody"; diff --git a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs index 85e3edf22e..c77243e1d0 100644 --- a/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.shielded_pool.v1.serde.rs @@ -3789,6 +3789,9 @@ impl serde::Serialize for SpendBody { if self.rk.is_some() { len += 1; } + if !self.encrypted_backref.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.core.component.shielded_pool.v1.SpendBody", len)?; if let Some(v) = self.balance_commitment.as_ref() { struct_ser.serialize_field("balanceCommitment", v)?; @@ -3799,6 +3802,10 @@ impl serde::Serialize for SpendBody { if let Some(v) = self.rk.as_ref() { struct_ser.serialize_field("rk", v)?; } + if !self.encrypted_backref.is_empty() { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("encryptedBackref", pbjson::private::base64::encode(&self.encrypted_backref).as_str())?; + } struct_ser.end() } } @@ -3813,6 +3820,8 @@ impl<'de> serde::Deserialize<'de> for SpendBody { "balanceCommitment", "nullifier", "rk", + "encrypted_backref", + "encryptedBackref", ]; #[allow(clippy::enum_variant_names)] @@ -3820,6 +3829,7 @@ impl<'de> serde::Deserialize<'de> for SpendBody { BalanceCommitment, Nullifier, Rk, + EncryptedBackref, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -3845,6 +3855,7 @@ impl<'de> serde::Deserialize<'de> for SpendBody { "balanceCommitment" | "balance_commitment" => Ok(GeneratedField::BalanceCommitment), "nullifier" => Ok(GeneratedField::Nullifier), "rk" => Ok(GeneratedField::Rk), + "encryptedBackref" | "encrypted_backref" => Ok(GeneratedField::EncryptedBackref), _ => Ok(GeneratedField::__SkipField__), } } @@ -3867,6 +3878,7 @@ impl<'de> serde::Deserialize<'de> for SpendBody { let mut balance_commitment__ = None; let mut nullifier__ = None; let mut rk__ = None; + let mut encrypted_backref__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::BalanceCommitment => { @@ -3887,6 +3899,14 @@ impl<'de> serde::Deserialize<'de> for SpendBody { } rk__ = map_.next_value()?; } + GeneratedField::EncryptedBackref => { + if encrypted_backref__.is_some() { + return Err(serde::de::Error::duplicate_field("encryptedBackref")); + } + encrypted_backref__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -3896,6 +3916,7 @@ impl<'de> serde::Deserialize<'de> for SpendBody { balance_commitment: balance_commitment__, nullifier: nullifier__, rk: rk__, + encrypted_backref: encrypted_backref__.unwrap_or_default(), }) } } diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 9287b24221..06cd62581e 100644 Binary files a/crates/proto/src/gen/proto_descriptor.bin.no_lfs and b/crates/proto/src/gen/proto_descriptor.bin.no_lfs differ diff --git a/proto/penumbra/penumbra/core/component/ibc/v1/ibc.proto b/proto/penumbra/penumbra/core/component/ibc/v1/ibc.proto index 23f3ba1f86..17f92c443e 100644 --- a/proto/penumbra/penumbra/core/component/ibc/v1/ibc.proto +++ b/proto/penumbra/penumbra/core/component/ibc/v1/ibc.proto @@ -55,8 +55,13 @@ message Ics20Withdrawal { // for compatability with chains that expect to be able to parse the return address as bech32. bool use_compat_address = 8 [deprecated = true]; + // Arbitrary string data to be included in the `memo` field + // of the ICS-20 FungibleTokenPacketData for this withdrawal. + // Commonly used for packet forwarding support, or other protocols that may support usage of the memo field. + string ics20_memo = 9; + // Whether to use a transparent address (bech32, 32-byte) for the return address in the withdrawal. - bool use_transparent_address = 9; + bool use_transparent_address = 10; } message ClientData { diff --git a/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto b/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto index f93427e8e8..d2c9cb55f7 100644 --- a/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto +++ b/proto/penumbra/penumbra/core/component/shielded_pool/v1/shielded_pool.proto @@ -42,7 +42,7 @@ message FmdMetaParameters { } // How much time users have to transition to new parameters. - uint64 fmd_grace_period_blocks = 1; + uint64 fmd_grace_period_blocks = 1; // The algorithm governing how the parameters change. oneof algorithm { uint32 fixed_precision_bits = 2; @@ -154,6 +154,8 @@ message SpendBody { sct.v1.Nullifier nullifier = 6; // The randomized validating key for the spend authorization signature. crypto.decaf377_rdsa.v1.SpendVerificationKey rk = 4; + // An encryption of the commitment of the input note to the sender's OVK. + bytes encrypted_backref = 7; } message SpendView { message Visible {