From 33db8ae7f4e5e684a23596b50b5841d41dfbca10 Mon Sep 17 00:00:00 2001 From: Michael Zaikin Date: Thu, 5 Feb 2026 17:39:23 +0000 Subject: [PATCH] feat(indexer): outgoing channel primitives --- .../src/privacy_pool/decryption.rs | 38 ++++++- .../discovery-core/src/privacy_pool/hashes.rs | 103 ++++++++++++++++++ .../src/privacy_pool/storage_slots.rs | 18 +++ .../discovery-core/src/privacy_pool/types.rs | 9 ++ .../discovery-core/src/privacy_pool/views.rs | 24 +++- 5 files changed, 188 insertions(+), 4 deletions(-) diff --git a/crates/discovery-core/src/privacy_pool/decryption.rs b/crates/discovery-core/src/privacy_pool/decryption.rs index bcd13e4b4..ed9f2da56 100644 --- a/crates/discovery-core/src/privacy_pool/decryption.rs +++ b/crates/discovery-core/src/privacy_pool/decryption.rs @@ -7,10 +7,13 @@ use starknet_types_core::{curve::AffinePoint, felt::Felt}; use thiserror::Error; use super::hashes::{ - compute_enc_amount_hash, compute_enc_channel_key_hash, compute_enc_sender_addr_hash, - compute_enc_token_hash, + compute_enc_amount_hash, compute_enc_channel_key_hash, compute_enc_recipient_addr_hash, + compute_enc_sender_addr_hash, compute_enc_token_hash, +}; +use super::types::{ + felt_low_u128, ChannelInfo, EncChannelInfo, EncOutgoingChannelInfo, EncSubchannelInfo, + SecretFelt, }; -use super::types::{felt_low_u128, ChannelInfo, EncChannelInfo, EncSubchannelInfo, SecretFelt}; /// Errors that can occur during decryption. #[derive(Debug, Error)] @@ -95,6 +98,19 @@ pub fn decrypt_note_amount( enc_amount.wrapping_sub(pad) } +/// Decrypts an outgoing channel's encrypted recipient address. +/// +/// `recipient_addr = enc_recipient_addr - hash(ENC_RECIPIENT_ADDR_TAG, sender_addr, viewing_key, index, 0, salt)` +pub fn decrypt_outgoing_recipient_addr( + enc: &EncOutgoingChannelInfo, + sender_addr: Felt, + viewing_key: &SecretFelt, + index: u64, +) -> Felt { + let mask = compute_enc_recipient_addr_hash(sender_addr, viewing_key, index, enc.salt); + enc.enc_recipient_addr - mask +} + #[cfg(test)] mod tests { use super::*; @@ -163,4 +179,20 @@ mod tests { ); assert_eq!(amount, f.outputs.dec_note_amount as u128); } + + #[test] + fn test_decrypt_outgoing_recipient_addr_with_cairo_vectors() { + let f = load_cairo_ref_fixture(); + + let enc = EncOutgoingChannelInfo { + salt: f.outputs.enc_outgoing_salt, + enc_recipient_addr: f.outputs.enc_outgoing_recipient_addr, + }; + + let key = SecretFelt::new(f.inputs.sender_private_key); + let recipient = + decrypt_outgoing_recipient_addr(&enc, f.inputs.sender, &key, f.inputs.index); + + assert_eq!(recipient, f.inputs.recipient); + } } diff --git a/crates/discovery-core/src/privacy_pool/hashes.rs b/crates/discovery-core/src/privacy_pool/hashes.rs index 5647363b2..6b7766952 100644 --- a/crates/discovery-core/src/privacy_pool/hashes.rs +++ b/crates/discovery-core/src/privacy_pool/hashes.rs @@ -1,4 +1,6 @@ //! Hash functions and domain separation tags. +//! +//! TODO: rename primitives to match smart contract terminology use std::sync::LazyLock; @@ -29,6 +31,18 @@ static ENC_AMOUNT_TAG: LazyLock = LazyLock::new(|| short_string_to_felt("E /// Domain separation tag for nullifier derivation. static NULLIFIER_TAG: LazyLock = LazyLock::new(|| short_string_to_felt("NULLIFIER_TAG:V1")); +/// Domain separation tag for channel key derivation. +static CHANNEL_KEY_TAG: LazyLock = + LazyLock::new(|| short_string_to_felt("CHANNEL_KEY_TAG:V1")); + +/// Domain separation tag for outgoing channel id derivation. +static OUTGOING_CHANNEL_ID_TAG: LazyLock = + LazyLock::new(|| short_string_to_felt("OUTGOING_CHANNEL_ID_TAG:V1")); + +/// Domain separation tag for encrypted recipient address. +static ENC_RECIPIENT_ADDR_TAG: LazyLock = + LazyLock::new(|| short_string_to_felt("ENC_RECIPIENT_ADDR_TAG:V1")); + /// Converts a short string (up to 31 ASCII chars) to Felt. fn short_string_to_felt(s: &str) -> Felt { assert!( @@ -128,6 +142,60 @@ pub fn compute_nullifier( ]) } +/// Computes the channel key from sender credentials and recipient identity. +/// +/// `channel_key = hash(CHANNEL_KEY_TAG, sender_addr, viewing_key, recipient_addr, recipient_public_key)` +pub fn compute_channel_key( + sender_addr: Felt, + viewing_key: &super::types::SecretFelt, + recipient_addr: Felt, + recipient_public_key: Felt, +) -> Felt { + hash(&[ + *CHANNEL_KEY_TAG, + sender_addr, + **viewing_key, + recipient_addr, + recipient_public_key, + ]) +} + +/// Computes the outgoing channel id for storage lookup. +/// +/// `outgoing_channel_id = hash(OUTGOING_CHANNEL_ID_TAG, sender_addr, viewing_key, index, 0)` +pub fn compute_outgoing_channel_id( + sender_addr: Felt, + viewing_key: &super::types::SecretFelt, + index: u64, +) -> Felt { + hash(&[ + *OUTGOING_CHANNEL_ID_TAG, + sender_addr, + **viewing_key, + Felt::from(index), + Felt::ZERO, + ]) +} + +/// Computes the encryption mask for the outgoing recipient address. +/// +/// `enc_recipient_addr_hash = hash(ENC_RECIPIENT_ADDR_TAG, sender_addr, viewing_key, index, 0, salt)` +pub fn compute_enc_recipient_addr_hash( + sender_addr: Felt, + viewing_key: &super::types::SecretFelt, + index: u64, + salt: Felt, +) -> Felt { + hash(&[ + *ENC_RECIPIENT_ADDR_TAG, + sender_addr, + **viewing_key, + Felt::from(index), + Felt::ZERO, + salt, + ]) +} + #[cfg(test)] mod tests { use super::*; @@ -207,4 +275,39 @@ mod tests { f.outputs.enc_amount_hash ); } + + #[test] + fn test_compute_channel_key() { + let f = load_cairo_ref_fixture(); + let key = SecretFelt::new(f.inputs.sender_private_key); + assert_eq!( + compute_channel_key( + f.inputs.sender, + &key, + f.inputs.recipient, + f.inputs.recipient_public_key, + ), + f.outputs.channel_key + ); + } + + #[test] + fn test_compute_outgoing_channel_id() { + let f = load_cairo_ref_fixture(); + let key = SecretFelt::new(f.inputs.sender_private_key); + assert_eq!( + compute_outgoing_channel_id(f.inputs.sender, &key, f.inputs.index), + f.outputs.outgoing_channel_id + ); + } + + #[test] + fn test_compute_enc_recipient_addr_hash() { + let f = load_cairo_ref_fixture(); + let key = SecretFelt::new(f.inputs.sender_private_key); + assert_eq!( + compute_enc_recipient_addr_hash(f.inputs.sender, &key, f.inputs.index, f.inputs.salt), + f.outputs.enc_recipient_addr_hash + ); + } } diff --git a/crates/discovery-core/src/privacy_pool/storage_slots.rs b/crates/discovery-core/src/privacy_pool/storage_slots.rs index 0bbfbfb14..205beb44d 100644 --- a/crates/discovery-core/src/privacy_pool/storage_slots.rs +++ b/crates/discovery-core/src/privacy_pool/storage_slots.rs @@ -25,6 +25,13 @@ pub struct EncSubchannelInfoSlots { pub enc_token: Felt, } +/// Storage slots for encrypted outgoing channel info (2 consecutive slots). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EncOutgoingChannelInfoSlots { + pub salt: Felt, + pub enc_recipient_addr: Felt, +} + /// Computes a storage variable address. /// /// Wraps `get_storage_var_address`, which only fails if the variable name @@ -99,6 +106,17 @@ pub fn subchannel_tokens(subchannel_id: Felt) -> EncSubchannelInfoSlots { } } +/// Storage slots for encrypted outgoing channel info. +/// Cairo: `outgoing_channels: Map` +/// EncOutgoingChannelInfo has 2 fields: salt and enc_recipient_addr. +pub fn outgoing_channels(outgoing_channel_id: Felt) -> EncOutgoingChannelInfoSlots { + let base = slot("outgoing_channels", &[outgoing_channel_id]); + EncOutgoingChannelInfoSlots { + salt: base, + enc_recipient_addr: base + Felt::ONE, + } +} + /// Storage slot for a note's existence. /// Cairo: `notes: LegacyMap` pub fn notes(note_id: Felt) -> Felt { diff --git a/crates/discovery-core/src/privacy_pool/types.rs b/crates/discovery-core/src/privacy_pool/types.rs index ee4fc1717..ab5781673 100644 --- a/crates/discovery-core/src/privacy_pool/types.rs +++ b/crates/discovery-core/src/privacy_pool/types.rs @@ -94,3 +94,12 @@ pub struct ChannelInfo { /// The sender's address. pub sender_addr: Felt, } + +/// Encrypted outgoing channel info stored in the contract. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EncOutgoingChannelInfo { + /// Salt generated by the sender for one-time key usage. + pub salt: Felt, + /// Encrypted recipient address. + pub enc_recipient_addr: Felt, +} diff --git a/crates/discovery-core/src/privacy_pool/views.rs b/crates/discovery-core/src/privacy_pool/views.rs index 0a462a231..8eff11a40 100644 --- a/crates/discovery-core/src/privacy_pool/views.rs +++ b/crates/discovery-core/src/privacy_pool/views.rs @@ -5,7 +5,7 @@ use num_traits::ToPrimitive; use starknet_types_core::felt::Felt; use super::storage_slots; -use super::types::{EncChannelInfo, EncPrivateKey, EncSubchannelInfo}; +use super::types::{EncChannelInfo, EncOutgoingChannelInfo, EncPrivateKey, EncSubchannelInfo}; use crate::storage_backend::{RawStorageAccess, StorageError}; /// Privacy contract view methods. @@ -33,6 +33,12 @@ pub trait IViews: Send + Sync { subchannel_id: Felt, ) -> Result; + /// Returns encrypted outgoing channel info for the given outgoing channel id. + async fn get_outgoing_channel_info( + &self, + outgoing_channel_id: Felt, + ) -> Result; + /// Returns the note value for the given note ID. async fn get_note(&self, note_id: Felt) -> Result; @@ -123,6 +129,22 @@ impl IViews for T { }) } + #[tracing::instrument(name = "get_outgoing_channel_info", level = "debug", skip(self))] + async fn get_outgoing_channel_info( + &self, + outgoing_channel_id: Felt, + ) -> Result { + let slots = storage_slots::outgoing_channels(outgoing_channel_id); + let values = self + .read_slots(vec![slots.salt, slots.enc_recipient_addr]) + .await?; + check_slots_len(&values, 2)?; + Ok(EncOutgoingChannelInfo { + salt: values[0], + enc_recipient_addr: values[1], + }) + } + #[tracing::instrument(name = "get_note", level = "debug", skip(self))] async fn get_note(&self, note_id: Felt) -> Result { let slot = storage_slots::notes(note_id);