From eb9c45bbb7440049f54b7c5d470a9738f80ba065 Mon Sep 17 00:00:00 2001 From: Oleksandr Pravdyvyi Date: Fri, 12 Sep 2025 16:00:57 +0300 Subject: [PATCH] feat: private token transfer --- Cargo.lock | 3 + common/Cargo.toml | 1 + common/src/rpc_primitives/requests.rs | 11 ++ common/src/sequencer_client/mod.rs | 25 +++- integration_tests/src/lib.rs | 10 +- .../key_management/ephemeral_key_holder.rs | 6 +- nssa/core/src/encryption/mod.rs | 4 +- .../src/encryption/shared_key_derivation.rs | 2 +- nssa/core/src/nullifier.rs | 2 +- sequencer_rpc/Cargo.toml | 1 + sequencer_rpc/src/process.rs | 20 ++- wallet/Cargo.toml | 1 + wallet/src/lib.rs | 121 +++++++++++++++--- 13 files changed, 171 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24e6b3f..9e6e45d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1058,6 +1058,7 @@ dependencies = [ "k256", "log", "nssa", + "nssa-core", "reqwest 0.11.27", "rs_merkle", "secp256k1-zkp", @@ -3935,6 +3936,7 @@ dependencies = [ "hex", "log", "nssa", + "nssa-core", "sequencer_core", "serde", "serde_json", @@ -4680,6 +4682,7 @@ dependencies = [ "clap", "common", "env_logger", + "k256", "key_protocol", "log", "nssa", diff --git a/common/Cargo.toml b/common/Cargo.toml index dcb5f60..0bb6491 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -16,6 +16,7 @@ sha2.workspace = true log.workspace = true elliptic-curve.workspace = true hex.workspace = true +nssa-core = { path = "../nssa/core", features = ["host"] } [dependencies.secp256k1-zkp] workspace = true diff --git a/common/src/rpc_primitives/requests.rs b/common/src/rpc_primitives/requests.rs index a566ee2..7ac271a 100644 --- a/common/src/rpc_primitives/requests.rs +++ b/common/src/rpc_primitives/requests.rs @@ -53,6 +53,11 @@ pub struct GetAccountDataRequest { pub address: String, } +#[derive(Serialize, Deserialize, Debug)] +pub struct GetProofByCommitmentRequest { + pub commitment: nssa_core::Commitment, +} + parse_request!(HelloRequest); parse_request!(RegisterAccountRequest); parse_request!(SendTxRequest); @@ -64,6 +69,7 @@ parse_request!(GetAccountBalanceRequest); parse_request!(GetTransactionByHashRequest); parse_request!(GetAccountsNoncesRequest); parse_request!(GetAccountDataRequest); +parse_request!(GetProofByCommitmentRequest); #[derive(Serialize, Deserialize, Debug)] pub struct HelloResponse { @@ -118,3 +124,8 @@ pub struct GetAccountDataResponse { pub program_owner: [u32; 8], pub data: Vec, } + +#[derive(Serialize, Deserialize, Debug)] +pub struct GetProofByCommitmentResponse { + pub membership_proof: Option, +} diff --git a/common/src/sequencer_client/mod.rs b/common/src/sequencer_client/mod.rs index bb78e50..896e377 100644 --- a/common/src/sequencer_client/mod.rs +++ b/common/src/sequencer_client/mod.rs @@ -8,8 +8,8 @@ use reqwest::Client; use serde_json::Value; use crate::rpc_primitives::requests::{ - GetAccountsNoncesRequest, GetAccountsNoncesResponse, GetTransactionByHashRequest, - GetTransactionByHashResponse, + GetAccountsNoncesRequest, GetAccountsNoncesResponse, GetProofByCommitmentRequest, + GetProofByCommitmentResponse, GetTransactionByHashRequest, GetTransactionByHashResponse, }; use crate::sequencer_client::json::AccountInitialData; use crate::transaction::{EncodedTransaction, NSSATransaction}; @@ -200,4 +200,25 @@ impl SequencerClient { Ok(resp_deser) } + + ///Get proof for commitment + pub async fn get_proof_for_commitment( + &self, + commitment: nssa_core::Commitment, + ) -> Result, SequencerClientError> { + let acc_req = GetProofByCommitmentRequest { commitment }; + + let req = serde_json::to_value(acc_req).unwrap(); + + let resp = self + .call_method_with_payload("get_proof_for_commitment", req) + .await + .unwrap(); + + let resp_deser = serde_json::from_value::(resp) + .unwrap() + .membership_proof; + + Ok(resp_deser) + } } diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index ac532ac..d3994ab 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -82,7 +82,7 @@ pub async fn post_test(residual: (ServerHandle, JoinHandle>, TempDir) } pub async fn test_success() { - let command = Command::SendNativeTokenTransfer { + let command = Command::SendNativeTokenTransferPublic { from: ACC_SENDER.to_string(), to: ACC_RECEIVER.to_string(), amount: 100, @@ -141,7 +141,7 @@ pub async fn test_success_move_to_another_account() { panic!("Failed to produce new account, not present in persistent accounts"); } - let command = Command::SendNativeTokenTransfer { + let command = Command::SendNativeTokenTransferPublic { from: ACC_SENDER.to_string(), to: new_persistent_account_addr.clone(), amount: 100, @@ -172,7 +172,7 @@ pub async fn test_success_move_to_another_account() { } pub async fn test_failure() { - let command = Command::SendNativeTokenTransfer { + let command = Command::SendNativeTokenTransferPublic { from: ACC_SENDER.to_string(), to: ACC_RECEIVER.to_string(), amount: 1000000, @@ -209,7 +209,7 @@ pub async fn test_failure() { } pub async fn test_success_two_transactions() { - let command = Command::SendNativeTokenTransfer { + let command = Command::SendNativeTokenTransferPublic { from: ACC_SENDER.to_string(), to: ACC_RECEIVER.to_string(), amount: 100, @@ -242,7 +242,7 @@ pub async fn test_success_two_transactions() { info!("First TX Success!"); - let command = Command::SendNativeTokenTransfer { + let command = Command::SendNativeTokenTransferPublic { from: ACC_SENDER.to_string(), to: ACC_RECEIVER.to_string(), amount: 100, diff --git a/key_protocol/src/key_management/ephemeral_key_holder.rs b/key_protocol/src/key_management/ephemeral_key_holder.rs index 108a41e..7d54b6d 100644 --- a/key_protocol/src/key_management/ephemeral_key_holder.rs +++ b/key_protocol/src/key_management/ephemeral_key_holder.rs @@ -37,9 +37,9 @@ impl EphemeralKeyHolder { pub fn calculate_shared_secret_sender( &self, - receiver_incoming_viewing_public_key: Scalar, - ) -> Scalar { - receiver_incoming_viewing_public_key * self.ephemeral_secret_key + receiver_incoming_viewing_public_key: AffinePoint, + ) -> AffinePoint { + (receiver_incoming_viewing_public_key * self.ephemeral_secret_key).into() } pub fn log(&self) { diff --git a/nssa/core/src/encryption/mod.rs b/nssa/core/src/encryption/mod.rs index b79e75c..9f66bce 100644 --- a/nssa/core/src/encryption/mod.rs +++ b/nssa/core/src/encryption/mod.rs @@ -6,7 +6,7 @@ use risc0_zkvm::sha::{Impl, Sha256}; use serde::{Deserialize, Serialize}; #[cfg(feature = "host")] -pub(crate) mod shared_key_derivation; +pub mod shared_key_derivation; #[cfg(feature = "host")] pub use shared_key_derivation::{EphemeralPublicKey, EphemeralSecretKey, IncomingViewingPublicKey}; @@ -14,7 +14,7 @@ pub use shared_key_derivation::{EphemeralPublicKey, EphemeralSecretKey, Incoming use crate::{Commitment, account::Account}; #[derive(Serialize, Deserialize, Clone)] -pub struct SharedSecretKey([u8; 32]); +pub struct SharedSecretKey(pub [u8; 32]); pub struct EncryptionScheme; diff --git a/nssa/core/src/encryption/shared_key_derivation.rs b/nssa/core/src/encryption/shared_key_derivation.rs index c735105..0fd4f1a 100644 --- a/nssa/core/src/encryption/shared_key_derivation.rs +++ b/nssa/core/src/encryption/shared_key_derivation.rs @@ -11,7 +11,7 @@ use k256::{ use crate::SharedSecretKey; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub struct Secp256k1Point(pub(crate) Vec); +pub struct Secp256k1Point(pub Vec); impl Secp256k1Point { pub fn from_scalar(value: [u8; 32]) -> Secp256k1Point { diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs index d1410de..86f8114 100644 --- a/nssa/core/src/nullifier.rs +++ b/nssa/core/src/nullifier.rs @@ -5,7 +5,7 @@ use crate::Commitment; #[derive(Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(any(feature = "host", test), derive(Debug, Clone, Hash))] -pub struct NullifierPublicKey(pub(super) [u8; 32]); +pub struct NullifierPublicKey(pub [u8; 32]); impl From<&NullifierSecretKey> for NullifierPublicKey { fn from(value: &NullifierSecretKey) -> Self { diff --git a/sequencer_rpc/Cargo.toml b/sequencer_rpc/Cargo.toml index 7972342..64c69af 100644 --- a/sequencer_rpc/Cargo.toml +++ b/sequencer_rpc/Cargo.toml @@ -12,6 +12,7 @@ actix-cors.workspace = true futures.workspace = true hex.workspace = true tempfile.workspace = true +nssa-core = { path = "../nssa/core", features = ["host"] } base64.workspace = true actix-web.workspace = true diff --git a/sequencer_rpc/src/process.rs b/sequencer_rpc/src/process.rs index 9123bcb..c2e2952 100644 --- a/sequencer_rpc/src/process.rs +++ b/sequencer_rpc/src/process.rs @@ -14,7 +14,8 @@ use common::{ requests::{ GetAccountBalanceRequest, GetAccountBalanceResponse, GetAccountDataRequest, GetAccountDataResponse, GetAccountsNoncesRequest, GetAccountsNoncesResponse, - GetInitialTestnetAccountsRequest, GetTransactionByHashRequest, + GetInitialTestnetAccountsRequest, GetProofByCommitmentRequest, + GetProofByCommitmentResponse, GetTransactionByHashRequest, GetTransactionByHashResponse, }, }, @@ -38,6 +39,7 @@ pub const GET_ACCOUNT_BALANCE: &str = "get_account_balance"; pub const GET_TRANSACTION_BY_HASH: &str = "get_transaction_by_hash"; pub const GET_ACCOUNTS_NONCES: &str = "get_accounts_nonces"; pub const GET_ACCOUNT_DATA: &str = "get_account_data"; +pub const GET_PROOF_FOR_COMMITMENT: &str = "get_proof_for_commitment"; pub const HELLO_FROM_SEQUENCER: &str = "HELLO_FROM_SEQUENCER"; @@ -255,6 +257,21 @@ impl JsonHandler { respond(helperstruct) } + /// Returns the commitment proof, corresponding to commitment + async fn process_get_proof_by_commitment(&self, request: Request) -> Result { + let get_proof_req = GetProofByCommitmentRequest::parse(Some(request.params))?; + + let membership_proof = { + let state = self.sequencer_state.lock().await; + state + .store + .state + .get_proof_for_commitment(&get_proof_req.commitment) + }; + let helperstruct = GetProofByCommitmentResponse { membership_proof }; + respond(helperstruct) + } + pub async fn process_request_internal(&self, request: Request) -> Result { match request.method.as_ref() { HELLO => self.process_temp_hello(request).await, @@ -267,6 +284,7 @@ impl JsonHandler { GET_ACCOUNTS_NONCES => self.process_get_accounts_nonces(request).await, GET_ACCOUNT_DATA => self.process_get_account_data(request).await, GET_TRANSACTION_BY_HASH => self.process_get_transaction_by_hash(request).await, + GET_PROOF_FOR_COMMITMENT => self.process_get_proof_by_commitment(request).await, _ => Err(RpcErr(RpcError::method_not_found(request.method))), } } diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 2caa7ff..8cabdda 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -14,6 +14,7 @@ tempfile.workspace = true clap.workspace = true nssa-core = { path = "../nssa/core" } base64.workspace = true +k256 = { version = "0.13.3" } [dependencies.key_protocol] path = "../key_protocol" diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 63ac708..c14fbf7 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -10,6 +10,8 @@ use common::{ use anyhow::Result; use chain_storage::WalletChainStore; use config::WalletConfig; +use k256::elliptic_curve::group::GroupEncoding; +use k256::elliptic_curve::sec1::ToEncodedPoint; use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use log::info; use nssa::Address; @@ -176,37 +178,86 @@ impl WalletCore { &[ ( nssa_core::NullifierPublicKey(from_keys.nullifer_public_key), - shared_secret, + nssa_core::SharedSecretKey( + shared_secret.to_bytes().as_slice().try_into().unwrap(), + ), ), ( nssa_core::NullifierPublicKey(to_keys.nullifer_public_key), - shared_secret, + nssa_core::SharedSecretKey( + shared_secret.to_bytes().as_slice().try_into().unwrap(), + ), ), ], &[( from_keys.private_key_holder.nullifier_secret_key, - state.get_proof_for_commitment(&sender_commitment).unwrap(), + self.sequencer_client + .get_proof_for_commitment(sender_commitment) + .await + .unwrap() + .unwrap(), )], &program, ) .unwrap(); - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![ - (sender_keys.npk(), sender_keys.ivk(), epk_1), - (recipient_keys.npk(), recipient_keys.ivk(), epk_2), - ], - output, - ) - .unwrap(); + let message = + nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( + vec![], + vec![], + vec![ + ( + nssa_core::NullifierPublicKey(from_keys.nullifer_public_key), + nssa_core::encryption::shared_key_derivation::Secp256k1Point( + from_keys + .incoming_viewing_public_key + .to_encoded_point(true) + .as_bytes() + .to_vec(), + ), + nssa_core::encryption::shared_key_derivation::Secp256k1Point( + eph_holder + .generate_ephemeral_public_key() + .to_encoded_point(true) + .as_bytes() + .to_vec(), + ), + ), + ( + nssa_core::NullifierPublicKey(to_keys.nullifer_public_key), + nssa_core::encryption::shared_key_derivation::Secp256k1Point( + to_keys + .incoming_viewing_public_key + .to_encoded_point(true) + .as_bytes() + .to_vec(), + ), + nssa_core::encryption::shared_key_derivation::Secp256k1Point( + eph_holder + .generate_ephemeral_public_key() + .to_encoded_point(true) + .as_bytes() + .to_vec(), + ), + ), + ], + output, + ) + .unwrap(); - let witness_set = WitnessSet::for_message(&message, proof, &[]); + let witness_set = + nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( + &message, + proof, + &[], + ); - let tx = PrivacyPreservingTransaction::new(message, witness_set); + let tx = nssa::privacy_preserving_transaction::PrivacyPreservingTransaction::new( + message, + witness_set, + ); - Ok(self.sequencer_client.send_tx_public(tx).await?) + Ok(self.sequencer_client.send_tx_private(tx).await?) } else { Err(ExecutionFailureKind::InsufficientFundsError) } @@ -231,7 +282,7 @@ impl WalletCore { } ///Poll transactions - pub async fn poll_public_native_token_transfer(&self, hash: String) -> Result { + pub async fn poll_native_token_transfer(&self, hash: String) -> Result { let transaction_encoded = self.poller.poll_tx(hash).await?; let tx_base64_decode = base64::engine::general_purpose::STANDARD.decode(transaction_encoded)?; @@ -246,7 +297,23 @@ impl WalletCore { #[clap(about)] pub enum Command { ///Send native token transfer from `from` to `to` for `amount` - SendNativeTokenTransfer { + /// + /// Public operation + SendNativeTokenTransferPublic { + ///from - valid 32 byte hex string + #[arg(long)] + from: String, + ///to - valid 32 byte hex string + #[arg(long)] + to: String, + ///amount - amount of balance to move + #[arg(long)] + amount: u128, + }, + ///Send native token transfer from `from` to `to` for `amount` + /// + /// Private operation + SendNativeTokenTransferPrivate { ///from - valid 32 byte hex string #[arg(long)] from: String, @@ -292,7 +359,7 @@ pub async fn execute_subcommand(command: Command) -> Result<()> { let mut wallet_core = WalletCore::start_from_config_update_chain(wallet_config)?; match command { - Command::SendNativeTokenTransfer { from, to, amount } => { + Command::SendNativeTokenTransferPublic { from, to, amount } => { let from = produce_account_addr_from_hex(from)?; let to = produce_account_addr_from_hex(to)?; @@ -302,10 +369,22 @@ pub async fn execute_subcommand(command: Command) -> Result<()> { info!("Results of tx send is {res:#?}"); - let transfer_tx = wallet_core - .poll_public_native_token_transfer(res.tx_hash) + let transfer_tx = wallet_core.poll_native_token_transfer(res.tx_hash).await?; + + info!("Transaction data is {transfer_tx:?}"); + } + Command::SendNativeTokenTransferPrivate { from, to, amount } => { + let from = produce_account_addr_from_hex(from)?; + let to = produce_account_addr_from_hex(to)?; + + let res = wallet_core + .send_private_native_token_transfer(from, to, amount) .await?; + info!("Results of tx send is {res:#?}"); + + let transfer_tx = wallet_core.poll_native_token_transfer(res.tx_hash).await?; + info!("Transaction data is {transfer_tx:?}"); } Command::RegisterAccountPublic {} => {