diff --git a/applications/tari_walletd/src/handlers/confidential.rs b/applications/tari_walletd/src/handlers/confidential.rs index 247097626f..b1a9bd9616 100644 --- a/applications/tari_walletd/src/handlers/confidential.rs +++ b/applications/tari_walletd/src/handlers/confidential.rs @@ -295,18 +295,18 @@ pub async fn handle_view_vault_balance( let mut lookup = IoReaderValueLookup::load(&mut file)?; block_in_place(|| { - sdk.confidential_crypto_api().try_brute_force_commitment_balances( + sdk.viewable_balance_api().try_brute_force_commitment_balances( &view_key.key, - commitments.values(), + commitments.values().filter_map(|o| o.viewable_balance.as_ref()), value_range, &mut lookup, ) })? }, None => block_in_place(|| { - sdk.confidential_crypto_api().try_brute_force_commitment_balances( + sdk.viewable_balance_api().try_brute_force_commitment_balances( &view_key.key, - commitments.values(), + commitments.values().filter_map(|o| o.viewable_balance.as_ref()), value_range, &mut AlwaysMissLookupTable, ) @@ -316,6 +316,10 @@ pub async fn handle_view_vault_balance( info!(target: LOG_TARGET, "Brute force balance lookup took {:.2?}", timer.elapsed()); Ok(ConfidentialViewVaultBalanceResponse { - balances: commitments.keys().copied().zip(balances).collect(), + balances: commitments + .iter() + .filter_map(|(id, o)| o.viewable_balance.as_ref().map(|_| *id)) + .zip(balances) + .collect(), }) } diff --git a/applications/tari_walletd/src/handlers/stealth_utxos.rs b/applications/tari_walletd/src/handlers/stealth_utxos.rs index 8c44f428d2..4236a6873f 100644 --- a/applications/tari_walletd/src/handlers/stealth_utxos.rs +++ b/applications/tari_walletd/src/handlers/stealth_utxos.rs @@ -1,13 +1,30 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause +use std::fs; + +use anyhow::anyhow; use axum_extra::headers::authorization::Bearer; +use indexmap::IndexMap; +use log::info; +use tari_ootle_wallet_crypto::{AlwaysMissLookupTable, IoReaderValueLookup}; +use tari_ootle_wallet_sdk::apis::key_manager::KeyBranch; +use tari_template_lib::models::UtxoAddress; use tari_wallet_daemon_client::{ permissions::JrpcPermission, - types::{StealthUtxosListRequest, StealthUtxosListResponse, UtxoInfo}, + types::{ + StealthUtxosDecryptValueRequest, + StealthUtxosDecryptValueResponse, + StealthUtxosListRequest, + StealthUtxosListResponse, + UtxoInfo, + }, }; +use tokio::{task::block_in_place, time::Instant}; + +use crate::handlers::{helpers::invalid_params, HandlerContext}; -use crate::handlers::HandlerContext; +const LOG_TARGET: &str = "tari::walletd::handlers::stealth_utxos"; pub async fn handle_list( context: &HandlerContext, @@ -36,3 +53,82 @@ pub async fn handle_list( .collect(), }) } + +pub async fn handle_decrypt_value( + context: &HandlerContext, + token: Option<&Bearer>, + req: StealthUtxosDecryptValueRequest, +) -> Result { + let sdk = context.wallet_sdk(); + context.check_auth(token, &[JrpcPermission::Admin])?; + + if req.ids.len() > 10 { + return Err(invalid_params( + "ids", + Some("Cannot request more than 10 UTXOs at a time"), + )); + } + + let utxo_ids = req + .ids + .into_iter() + .map(|id| UtxoAddress::new(req.resource_address, id)) + .map(Into::into) + .collect::>(); + + let substates = sdk.substate_api().get_substates_from_network(utxo_ids).await?; + + // Get view secret key + let view_key = sdk + .key_manager_api() + .derive_key(KeyBranch::ElgamalEncryptionViewKey, req.view_key_id)?; + + let value_range = req.minimum_expected_value.unwrap_or(0)..=req.maximum_expected_value.unwrap_or(10_000_000_000); + + // NOTE: we iterate in a random order (HashMap) but collect into a deterministic order (IndexMap) so that the + // results are always in the same order for the same input + let proofs = substates + .iter() + .filter_map(|(id, s)| { + let id = id.as_utxo_address().map(|a| a.into_contents().id)?; + let output = s + .substate_value() + .as_utxo() + .and_then(|u| u.output()) + .and_then(|o| o.output.viewable_balance.as_ref())?; + Some((id, output)) + }) + .collect::>(); + + let timer = Instant::now(); + let balances = match context.config().value_lookup_table_file.as_ref() { + Some(file) => { + let mut file = fs::File::open(file) + .map_err(|e| anyhow!("Unable to load value lookup file '{}': {e}", file.display()))?; + let mut lookup = IoReaderValueLookup::load(&mut file)?; + + block_in_place(|| { + sdk.viewable_balance_api().try_brute_force_commitment_balances( + &view_key.key, + proofs.values().copied(), // Copying the reference, not the ElgamalVerifiableBalanceBytes + value_range, + &mut lookup, + ) + })? + }, + None => block_in_place(|| { + sdk.viewable_balance_api().try_brute_force_commitment_balances( + &view_key.key, + proofs.values().copied(), + value_range, + &mut AlwaysMissLookupTable, + ) + })?, + }; + + info!(target: LOG_TARGET, "Brute force balance lookup took {:.2?}", timer.elapsed()); + + Ok(StealthUtxosDecryptValueResponse { + balances: proofs.into_keys().zip(balances).collect(), + }) +} diff --git a/applications/tari_walletd/src/jrpc_server.rs b/applications/tari_walletd/src/jrpc_server.rs index 4116b92166..2d14d19a94 100644 --- a/applications/tari_walletd/src/jrpc_server.rs +++ b/applications/tari_walletd/src/jrpc_server.rs @@ -182,13 +182,10 @@ async fn handler( "claim_fees" => call_handler(context, value, token, validator::handle_claim_validator_fees).await, _ => Ok(value.method_not_found(&value.method)), }, - Some(("stealth_utxos", method)) => - { - #[allow(clippy::collapsible_match)] - match method { - "list" => call_handler(context, value, token, stealth_utxos::handle_list).await, - _ => Ok(value.method_not_found(&value.method)), - } + Some(("stealth_utxos", method)) => match method { + "list" => call_handler(context, value, token, stealth_utxos::handle_list).await, + "decrypt_value" => call_handler(context, value, token, stealth_utxos::handle_decrypt_value).await, + _ => Ok(value.method_not_found(&value.method)), }, Some(("wallet", "get_info")) => call_handler(context, value, token, wallet::handle_get_info).await, _ => Ok(value.method_not_found(&value.method)), diff --git a/bindings/src/types/wallet-daemon-client/StealthUtxosDecryptValueRequest.ts b/bindings/src/types/wallet-daemon-client/StealthUtxosDecryptValueRequest.ts new file mode 100644 index 0000000000..3f8ea6e1a3 --- /dev/null +++ b/bindings/src/types/wallet-daemon-client/StealthUtxosDecryptValueRequest.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ResourceAddress } from "../ResourceAddress"; +import type { UtxoId } from "../UtxoId"; + +export type StealthUtxosDecryptValueRequest = { + resource_address: ResourceAddress; + ids: Array; + view_key_id: bigint; + minimum_expected_value: bigint | null; + maximum_expected_value: bigint | null; +}; diff --git a/bindings/src/types/wallet-daemon-client/StealthUtxosDecryptValueResponse.ts b/bindings/src/types/wallet-daemon-client/StealthUtxosDecryptValueResponse.ts new file mode 100644 index 0000000000..5c1c4ade01 --- /dev/null +++ b/bindings/src/types/wallet-daemon-client/StealthUtxosDecryptValueResponse.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { UtxoId } from "../UtxoId"; + +export type StealthUtxosDecryptValueResponse = { balances: { [key in UtxoId]?: bigint | null } }; diff --git a/bindings/src/wallet-daemon-client.ts b/bindings/src/wallet-daemon-client.ts index 83f0e38e4e..ddc1332386 100644 --- a/bindings/src/wallet-daemon-client.ts +++ b/bindings/src/wallet-daemon-client.ts @@ -27,6 +27,7 @@ export * from "./types/wallet-daemon-client/TransactionGetAllRequest"; export * from "./types/wallet-daemon-client/WebauthnAlreadyRegisteredRequest"; export * from "./types/wallet-daemon-client/AuthMethod"; export * from "./types/wallet-daemon-client/AuthLoginDenyResponse"; +export * from "./types/wallet-daemon-client/StealthUtxosDecryptValueResponse"; export * from "./types/wallet-daemon-client/MintFaucetNftRequest"; export * from "./types/wallet-daemon-client/AuthLoginDenyRequest"; export * from "./types/wallet-daemon-client/PublishTemplateRequest"; @@ -108,6 +109,7 @@ export * from "./types/wallet-daemon-client/ProofsCancelRequest"; export * from "./types/wallet-daemon-client/AccountSetDefaultResponse"; export * from "./types/wallet-daemon-client/GetNftRequest"; export * from "./types/wallet-daemon-client/SubstatesListResponse"; +export * from "./types/wallet-daemon-client/StealthUtxosDecryptValueRequest"; export * from "./types/wallet-daemon-client/AccountOrKeyIndex"; export * from "./types/wallet-daemon-client/WebauthnStartAuthResponse"; export * from "./types/wallet-daemon-client/TransferNftRequest"; diff --git a/clients/javascript/wallet_daemon_client/package.json b/clients/javascript/wallet_daemon_client/package.json index a9a501c370..541cbaefed 100644 --- a/clients/javascript/wallet_daemon_client/package.json +++ b/clients/javascript/wallet_daemon_client/package.json @@ -1,6 +1,6 @@ { "name": "@tari-project/wallet_jrpc_client", - "version": "1.9.1", + "version": "1.9.2", "description": "Tari wallet JSON-RPC client library", "homepage": "https://github.com/tari-project/tari-ootle#readme", "bugs": { diff --git a/clients/javascript/wallet_daemon_client/src/index.ts b/clients/javascript/wallet_daemon_client/src/index.ts index 3b2ce5491c..760ef633c0 100644 --- a/clients/javascript/wallet_daemon_client/src/index.ts +++ b/clients/javascript/wallet_daemon_client/src/index.ts @@ -6,7 +6,9 @@ import type { AccountGetDefaultRequest, AccountGetRequest, - AccountGetResponse, AccountsAssociateStealthResourceRequest, AccountsAssociateStealthResourceResponse, + AccountGetResponse, + AccountsAssociateStealthResourceRequest, + AccountsAssociateStealthResourceResponse, AccountsCreateFreeTestCoinsRequest, AccountsCreateFreeTestCoinsResponse, AccountsCreateRequest, @@ -16,7 +18,9 @@ import type { AccountsGetBalancesRequest, AccountsGetBalancesResponse, AccountsListRequest, - AccountsListResponse, AccountsRenameRequest, AccountsRenameResponse, + AccountsListResponse, + AccountsRenameRequest, + AccountsRenameResponse, AccountsTransferRequest, AccountsTransferResponse, AuthGetAllJwtRequest, @@ -50,7 +54,11 @@ import type { rejectReasonToString, SettingsGetResponse, SettingsSetRequest, - SettingsSetResponse, StealthTransferRequest, StealthTransferResponse, StealthUtxosListRequest, + SettingsSetResponse, + StealthTransferRequest, + StealthTransferResponse, + StealthUtxosDecryptValueRequest, StealthUtxosDecryptValueResponse, + StealthUtxosListRequest, StealthUtxosListResponse, stringToSubstateId, substateIdToString, @@ -342,6 +350,10 @@ export class WalletDaemonClient { } + public stealthUtxosDecryptValue(params: StealthUtxosDecryptValueRequest): Promise { + return this.__invokeRpc("stealth_utxos.decrypt_value", params); + } + async __invokeRpc(method: string, params: object = null) { const id = this.id++; const response = await this.transport.sendRequest( diff --git a/clients/wallet_daemon_client/src/lib.rs b/clients/wallet_daemon_client/src/lib.rs index eb16c5f65f..7987ce1695 100644 --- a/clients/wallet_daemon_client/src/lib.rs +++ b/clients/wallet_daemon_client/src/lib.rs @@ -114,6 +114,8 @@ use crate::{ SettingsGetResponse, StealthTransferRequest, StealthTransferResponse, + StealthUtxosDecryptValueRequest, + StealthUtxosDecryptValueResponse, StealthUtxosListRequest, StealthUtxosListResponse, TransactionGetAllRequest, @@ -479,6 +481,13 @@ impl WalletDaemonClient { self.send_request("stealth_utxos.list", request.borrow()).await } + pub async fn stealth_utxos_decrypt_value>( + &mut self, + request: T, + ) -> Result { + self.send_request("stealth_utxos.decrypt_value", request.borrow()).await + } + pub async fn get_settings(&mut self) -> Result { self.send_request("settings.get", &json!({})).await } diff --git a/clients/wallet_daemon_client/src/types.rs b/clients/wallet_daemon_client/src/types.rs index cc473bfeaa..9ccd57d1bc 100644 --- a/clients/wallet_daemon_client/src/types.rs +++ b/clients/wallet_daemon_client/src/types.rs @@ -52,7 +52,15 @@ use tari_ootle_wallet_sdk::{ }; use tari_template_abi::{FunctionDef, TemplateDef}; use tari_template_lib::{ - models::{ConfidentialOutputStatement, EncryptedData, NonFungibleId, ResourceAddress, UtxoAddress, VaultId}, + models::{ + ConfidentialOutputStatement, + EncryptedData, + NonFungibleId, + ResourceAddress, + UtxoAddress, + UtxoId, + VaultId, + }, prelude::{ComponentAddress, ConfidentialWithdrawProof, ResourceType, RistrettoPublicKeyBytes}, types::{crypto::PedersenCommitmentBytes, Amount, TemplateAddress}, }; @@ -1118,3 +1126,19 @@ pub struct UtxoInfo { pub is_frozen: bool, pub is_on_chain: bool, } + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] +pub struct StealthUtxosDecryptValueRequest { + pub resource_address: ResourceAddress, + pub ids: Vec, + pub view_key_id: u64, + pub minimum_expected_value: Option, + pub maximum_expected_value: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] +pub struct StealthUtxosDecryptValueResponse { + pub balances: HashMap>, +} diff --git a/crates/template_lib/src/models/utxo.rs b/crates/template_lib/src/models/utxo.rs index fae2f17da3..e770d3fd61 100644 --- a/crates/template_lib/src/models/utxo.rs +++ b/crates/template_lib/src/models/utxo.rs @@ -35,6 +35,10 @@ impl UtxoAddress { pub fn id(&self) -> &UtxoId { &self.0.inner().id } + + pub fn into_contents(self) -> UtxoAddressContents { + self.0.into_inner() + } } impl FromStr for UtxoAddress { @@ -120,8 +124,8 @@ impl Display for UtxoId { #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] #[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] pub struct UtxoAddressContents { - resource_address: ResourceAddress, - id: UtxoId, + pub resource_address: ResourceAddress, + pub id: UtxoId, } #[cfg(feature = "borsh")] diff --git a/crates/wallet/sdk/src/apis/confidential_crypto.rs b/crates/wallet/sdk/src/apis/confidential_crypto.rs index 7508e725d1..71be0868a6 100644 --- a/crates/wallet/sdk/src/apis/confidential_crypto.rs +++ b/crates/wallet/sdk/src/apis/confidential_crypto.rs @@ -1,14 +1,8 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::ops::RangeInclusive; - use tari_common_types::types::PrivateKey; use tari_crypto::ristretto::RistrettoPublicKey; -use tari_engine_types::{ - crypto::{ElgamalVerifiableBalance, PrivateOutput, ValueLookupTable}, - ConvertFromByteType, -}; use tari_ootle_wallet_crypto::{ confidential, encrypted_data::{encrypt_value_and_mask, extract_value_and_mask, unblind_output}, @@ -119,39 +113,6 @@ impl ConfidentialCryptoApi { )?; Ok(unmasked_output) } - - pub fn try_brute_force_commitment_balances<'a, TLookup, TOutputsIter>( - &self, - secret_view_key: &PrivateKey, - outputs: TOutputsIter, - value_range: RangeInclusive, - lookup: &mut TLookup, - ) -> Result>, ConfidentialCryptoApiError> - where - TLookup: ValueLookupTable, - TOutputsIter: Iterator, - { - let outputs_viewable_balance_decompressed = outputs - .filter_map(|output| output.viewable_balance.as_ref()) - .map(ElgamalVerifiableBalance::convert_from_byte_type) - .collect::, _>>() - .map_err(|_| WalletCryptoError::InvalidArgument { - name: "outputs", - details: "Malformed viewable balance in output when decompressing ElgamalVerifiableBalance for brute \ - forcing" - .to_string(), - })?; - - let results = ElgamalVerifiableBalance::batched_brute_force( - secret_view_key, - value_range, - lookup, - &outputs_viewable_balance_decompressed, - ) - .map_err(|e| ConfidentialCryptoApiError::ValueLookupTableError { details: e.to_string() })?; - - Ok(results) - } } #[derive(Debug, thiserror::Error)] @@ -160,8 +121,6 @@ pub enum ConfidentialCryptoApiError { WalletCryptoError(#[from] WalletCryptoError), #[error("Confidential proof error: {0}")] ConfidentialProofError(#[from] ConfidentialProofError), - #[error("Value lookup table error: {details}")] - ValueLookupTableError { details: String }, #[error("Negative amount")] NegativeAmount, } diff --git a/crates/wallet/sdk/src/apis/mod.rs b/crates/wallet/sdk/src/apis/mod.rs index 9f391a3ff3..9fbfa0d463 100644 --- a/crates/wallet/sdk/src/apis/mod.rs +++ b/crates/wallet/sdk/src/apis/mod.rs @@ -15,3 +15,4 @@ pub mod stealth_transfer; pub mod substate; pub mod template; pub mod transaction; +pub mod viewable_balance; diff --git a/crates/wallet/sdk/src/apis/stealth_crypto.rs b/crates/wallet/sdk/src/apis/stealth_crypto.rs index 5773a2285b..22884e7b3b 100644 --- a/crates/wallet/sdk/src/apis/stealth_crypto.rs +++ b/crates/wallet/sdk/src/apis/stealth_crypto.rs @@ -1,17 +1,12 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::ops::RangeInclusive; - use log::*; use tari_crypto::{ commitment::HomomorphicCommitmentFactory, ristretto::{pedersen::PedersenCommitment, RistrettoPublicKey, RistrettoSchnorr, RistrettoSecretKey}, }; -use tari_engine_types::{ - crypto::{get_commitment_factory, ElgamalVerifiableBalance, PrivateOutput, ValueLookupTable}, - ConvertFromByteType, -}; +use tari_engine_types::{crypto::get_commitment_factory, ConvertFromByteType}; use tari_ootle_common_types::{base_layer_hashing::ownership_proof_hasher64, Network}; use tari_ootle_wallet_crypto::{ confidential, @@ -146,39 +141,6 @@ impl StealthCryptoApi { Ok(unmasked_output) } - pub fn try_brute_force_commitment_balances<'a, TLookup, TOutputsIter>( - &self, - secret_view_key: &RistrettoSecretKey, - outputs: TOutputsIter, - value_range: RangeInclusive, - lookup: &mut TLookup, - ) -> Result>, StealthCryptoApiError> - where - TLookup: ValueLookupTable, - TOutputsIter: Iterator, - { - let outputs_viewable_balance_decompressed = outputs - .filter_map(|output| output.viewable_balance.as_ref()) - .map(ElgamalVerifiableBalance::convert_from_byte_type) - .collect::, _>>() - .map_err(|_| WalletCryptoError::InvalidArgument { - name: "outputs", - details: "Malformed viewable balance in output when decompressing ElgamalVerifiableBalance for brute \ - forcing" - .to_string(), - })?; - - let results = ElgamalVerifiableBalance::batched_brute_force( - secret_view_key, - value_range, - lookup, - &outputs_viewable_balance_decompressed, - ) - .map_err(|e| StealthCryptoApiError::ValueLookupTableError { details: e.to_string() })?; - - Ok(results) - } - pub fn validate_burn_claim_ownership_proof( &self, network: Network, diff --git a/crates/wallet/sdk/src/apis/viewable_balance.rs b/crates/wallet/sdk/src/apis/viewable_balance.rs new file mode 100644 index 0000000000..bd6af20956 --- /dev/null +++ b/crates/wallet/sdk/src/apis/viewable_balance.rs @@ -0,0 +1,56 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::ops::RangeInclusive; + +use tari_crypto::ristretto::RistrettoSecretKey; +use tari_engine_types::{ + crypto::{ElgamalVerifiableBalance, ElgamalVerifiableBalanceBytes, ValueLookupTable}, + ConvertFromByteType, +}; +use tari_ootle_wallet_crypto::WalletCryptoError; + +#[derive(Debug, Clone)] +pub struct ViewableBalanceApi; + +impl ViewableBalanceApi { + pub fn try_brute_force_commitment_balances<'a, TLookup, TProofsIter>( + &self, + secret_view_key: &RistrettoSecretKey, + proofs: TProofsIter, + value_range: RangeInclusive, + lookup: &mut TLookup, + ) -> Result>, ViewableBalanceApiError> + where + TLookup: ValueLookupTable, + TProofsIter: Iterator, + { + let outputs_viewable_balance_decompressed = proofs + .map(ElgamalVerifiableBalance::convert_from_byte_type) + .collect::, _>>() + .map_err(|_| WalletCryptoError::InvalidArgument { + name: "proofs", + details: "Malformed viewable balance in output when decompressing ElgamalVerifiableBalance for brute \ + forcing" + .to_string(), + })?; + + let results = ElgamalVerifiableBalance::batched_brute_force( + secret_view_key, + value_range, + lookup, + &outputs_viewable_balance_decompressed, + ) + .map_err(|e| ViewableBalanceApiError::ValueLookupTableError { details: e.to_string() })?; + + Ok(results) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ViewableBalanceApiError { + #[error(transparent)] + WalletCryptoError(#[from] WalletCryptoError), + #[error("ValueLookupTable error: {details}")] + ValueLookupTableError { details: String }, +} diff --git a/crates/wallet/sdk/src/sdk.rs b/crates/wallet/sdk/src/sdk.rs index 8139372178..69ff5ac31d 100644 --- a/crates/wallet/sdk/src/sdk.rs +++ b/crates/wallet/sdk/src/sdk.rs @@ -36,6 +36,7 @@ use crate::{ substate::SubstatesApi, template::TemplateApi, transaction::TransactionApi, + viewable_balance::ViewableBalanceApi, }, network::{StatusResponseError, WalletNetworkInterface}, storage::{WalletStorageError, WalletStore}, @@ -222,6 +223,10 @@ where TemplateApi::new(&self.store) } + pub fn viewable_balance_api(&self) -> ViewableBalanceApi { + ViewableBalanceApi + } + /// Tries to get encrypted cipher seed from DB and decrypts it using OS keyring if possible. fn load_cipher_seed(&mut self) -> Result>, WalletSdkError> { if let Some(ref cipher_seed) = self.loaded_cipher_seed {