feat: private token transfer

This commit is contained in:
Oleksandr Pravdyvyi 2025-09-12 16:00:57 +03:00
parent 854d96af72
commit eb9c45bbb7
No known key found for this signature in database
GPG Key ID: 9F8955C63C443871
13 changed files with 171 additions and 36 deletions

3
Cargo.lock generated
View File

@ -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",

View File

@ -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

View File

@ -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<u8>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetProofByCommitmentResponse {
pub membership_proof: Option<nssa_core::MembershipProof>,
}

View File

@ -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<Option<nssa_core::MembershipProof>, 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::<GetProofByCommitmentResponse>(resp)
.unwrap()
.membership_proof;
Ok(resp_deser)
}
}

View File

@ -82,7 +82,7 @@ pub async fn post_test(residual: (ServerHandle, JoinHandle<Result<()>>, 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,

View File

@ -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) {

View File

@ -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;

View File

@ -11,7 +11,7 @@ use k256::{
use crate::SharedSecretKey;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct Secp256k1Point(pub(crate) Vec<u8>);
pub struct Secp256k1Point(pub Vec<u8>);
impl Secp256k1Point {
pub fn from_scalar(value: [u8; 32]) -> Secp256k1Point {

View File

@ -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 {

View File

@ -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

View File

@ -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<Value, RpcErr> {
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<Value, RpcErr> {
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))),
}
}

View File

@ -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"

View File

@ -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<NSSATransaction> {
pub async fn poll_native_token_transfer(&self, hash: String) -> Result<NSSATransaction> {
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 {} => {