From 02dc4c17a1b3448efe6cbb58e6f0cc90cc7a4a03 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Wed, 15 Oct 2025 13:48:04 +0400 Subject: [PATCH 1/4] fix(wallet/sdk): use key store in key manager api --- .../tari_walletd/src/handlers/nfts.rs | 2 +- .../tari_walletd/src/handlers/transaction.rs | 2 +- .../tari_walletd/src/handlers/validator.rs | 23 +-- crates/engine/src/runtime/working_state.rs | 25 +-- crates/engine/tests/stealth.rs | 5 +- crates/engine_types/src/crypto/messages.rs | 29 ++- crates/engine_types/src/crypto/mod.rs | 2 - crates/engine_types/src/crypto/utxo_spend.rs | 39 ---- crates/engine_types/src/hash.rs | 183 ++++++++++++++++++ crates/engine_types/src/lib.rs | 2 + crates/engine_types/src/stealth/transfer.rs | 80 +++++--- .../template_lib_types/src/crypto/scalar.rs | 4 + .../template_lib_types/src/encrypted_data.rs | 4 + crates/template_lib_types/src/max_bytes.rs | 8 +- crates/transaction/src/builder/mod.rs | 6 +- crates/wallet/crypto/src/balance_proof.rs | 11 +- crates/wallet/crypto/src/stealth.rs | 9 +- ...ement.rs => stealth_transfer_statement.rs} | 89 ++++++++- crates/wallet/sdk/src/apis/key_manager.rs | 82 +++----- crates/wallet/sdk/src/key_managers/backend.rs | 4 +- crates/wallet/sdk/src/key_managers/local.rs | 34 +--- crates/wallet/sdk/src/local_key_store.rs | 32 ++- crates/wallet/sdk/src/models/key.rs | 1 - crates/wallet/sdk/src/sdk.rs | 18 +- utilities/tariswap_test_bench/src/tariswap.rs | 3 +- 25 files changed, 485 insertions(+), 212 deletions(-) delete mode 100644 crates/engine_types/src/crypto/utxo_spend.rs create mode 100644 crates/engine_types/src/hash.rs rename crates/wallet/crypto/tests/{output_statement.rs => stealth_transfer_statement.rs} (56%) diff --git a/applications/tari_walletd/src/handlers/nfts.rs b/applications/tari_walletd/src/handlers/nfts.rs index 7266a6b7b2..eb6aa9c023 100644 --- a/applications/tari_walletd/src/handlers/nfts.rs +++ b/applications/tari_walletd/src/handlers/nfts.rs @@ -303,7 +303,7 @@ pub async fn handle_transfer( .with_inputs(inputs.into_iter().map(|input| input.into_unversioned())) // Seal signer is the fee payer account .with_authorized_seal_signer() - .then(|builder| { + .map(|builder| { sdk.local_signer_api().sign_with_context( KeyBranch::Account, account_owner_key_id, diff --git a/applications/tari_walletd/src/handlers/transaction.rs b/applications/tari_walletd/src/handlers/transaction.rs index 36a143eb14..f5bd3f6bd4 100644 --- a/applications/tari_walletd/src/handlers/transaction.rs +++ b/applications/tari_walletd/src/handlers/transaction.rs @@ -327,7 +327,7 @@ pub async fn handle_submit_manifest( } }) .with_instructions(instructions.instructions) - .then(|builder| { + .map(|builder| { if signing_key_id == account_owner_key_id { Ok(builder) } else { diff --git a/applications/tari_walletd/src/handlers/validator.rs b/applications/tari_walletd/src/handlers/validator.rs index 440e4c5531..2bdb1b3eea 100644 --- a/applications/tari_walletd/src/handlers/validator.rs +++ b/applications/tari_walletd/src/handlers/validator.rs @@ -179,26 +179,23 @@ pub async fn handle_claim_validator_fees( .with_inputs(inputs.into_iter().map(|input| input.into_unversioned())) .with_inputs(fee_pool_addresses.map(SubstateRequirement::unversioned)) .add_input(XTR) - .then(|builder| { + .map(|builder| { if let Some(index) = req.claim_key_index { if claim_public_key == *account.address.account_public_key() { - builder + Ok(builder) } else { // If the claim key is different from the account secret, we need to sign with both - sdk.local_signer_api() - .sign_with_context( - KeyBranch::Account, - KeyId::derived(index), - account.address.account_public_key(), - builder.with_authorized_seal_signer(), - ) - // We happen to know that signing with a derived key is infallible - .expect("Signing with should work") + sdk.local_signer_api().sign_with_context( + KeyBranch::Account, + KeyId::derived(index), + account.address.account_public_key(), + builder.with_authorized_seal_signer(), + ) } } else { - builder + Ok(builder) } - }) + })? .build(); let transaction = sdk diff --git a/crates/engine/src/runtime/working_state.rs b/crates/engine/src/runtime/working_state.rs index 791c77b95e..8fe81b9ddb 100644 --- a/crates/engine/src/runtime/working_state.rs +++ b/crates/engine/src/runtime/working_state.rs @@ -14,7 +14,7 @@ use tari_crypto::ristretto::RistrettoPublicKey; use tari_engine_types::{ bucket::Bucket, component::ComponentHeader, - crypto::verify_utxo_spend_permission, + crypto::messages, events::Event, fees::FeeReceipt, id_provider::{IdProvider, ObjectIds}, @@ -27,6 +27,7 @@ use tari_engine_types::{ resource::Resource, resource_container::{ResourceContainer, ResourceError}, stealth, + stealth::ValidatedStealthTransfer, substate::{Substate, SubstateDiff, SubstateId, SubstateValue}, transaction_receipt::TransactionReceipt, vault::Vault, @@ -49,7 +50,6 @@ use tari_template_lib::{ NonFungibleAddress, ProofId, ResourceAddress, - StealthInputsStatement, StealthTransferStatement, UtxoAddress, VaultId, @@ -267,15 +267,18 @@ impl WorkingState { Ok(resource) } - pub fn spend_stealth_utxos( + pub fn validate_and_spend_stealth_utxos( &mut self, resource_address: ResourceAddress, - stmt: &StealthInputsStatement, - ) -> Result<(), RuntimeError> { - for input in &stmt.inputs { + stmt: &StealthTransferStatement, + view_key: Option<&RistrettoPublicKey>, + ) -> Result { + let metadata_hash = messages::stealth_statement_metadata64(&stmt.outputs_statement); + for input in &stmt.inputs_statement.inputs { let address = UtxoAddress::new(resource_address, input.commitment.into()); let lock_id = self.store.try_lock(&address.clone().into(), LockFlag::Write)?; let utxo = self.store.down_utxo(lock_id)?; + self.store.try_unlock(lock_id)?; if utxo.is_frozen() { return Err(ResourceError::InvalidSpend { details: format!("Utxo {} is frozen", address), @@ -287,9 +290,11 @@ impl WorkingState { details: format!("Utxo {} is burnt", address), })?; - verify_utxo_spend_permission(output, input)?; + stealth::validate_ownership_proof(output, input, &metadata_hash)?; } - Ok(()) + + let valid_transfer = stealth::validate_transfer_balance(stmt, view_key)?; + Ok(valid_transfer) } pub fn get_non_fungible(&self, locked: &LockedSubstate) -> Result<&NonFungibleContainer, RuntimeError> { @@ -1526,8 +1531,6 @@ impl WorkingState { }, } - self.spend_stealth_utxos(resource_address, &statement.inputs_statement)?; - let resource = self.get_resource(&resource_lock)?; let view_key = resource .view_key() @@ -1541,7 +1544,7 @@ impl WorkingState { } })?; - let valid_transfer = stealth::validate_transfer(&statement, view_key.as_ref())?; + let valid_transfer = self.validate_and_spend_stealth_utxos(resource_address, &statement, view_key.as_ref())?; for output in valid_transfer.outputs { let address = UtxoAddress::new(resource_address, output.output.commitment.to_byte_type().into()); diff --git a/crates/engine/tests/stealth.rs b/crates/engine/tests/stealth.rs index 64c8644ca6..c6f4518994 100644 --- a/crates/engine/tests/stealth.rs +++ b/crates/engine/tests/stealth.rs @@ -334,7 +334,7 @@ fn transfer_invalid_range_proof_in_statement() { .agg_range_proof .clone() .into_vec(); - rp[100] = rp[100].wrapping_add(1); // Corrupt the range proof + rp[100] ^= 0xFF; // Corrupt the range proof transfer_from_faucet.statement.outputs_statement.agg_range_proof = rp.try_into().unwrap(); let reason = test.execute_expect_failure( @@ -346,7 +346,8 @@ fn transfer_invalid_range_proof_in_statement() { vec![], ); - assert_reject_reason(reason, "Invalid range proof"); + // The ownership proof fails to validate first due to output malleation so range proof isnt validated. + assert_reject_reason(reason, "Invalid ownership proof"); } #[test] diff --git a/crates/engine_types/src/crypto/messages.rs b/crates/engine_types/src/crypto/messages.rs index 8932422251..fb9a6c4ee3 100644 --- a/crates/engine_types/src/crypto/messages.rs +++ b/crates/engine_types/src/crypto/messages.rs @@ -8,32 +8,37 @@ use tari_template_lib::{ types::Amount, }; -use crate::hashing::{engine_hasher64, EngineHashDomainLabel}; +use crate::{ + hashing::{engine_hasher64, EngineHashDomainLabel}, + Hash64, +}; pub fn confidential_withdraw64( excess: &RistrettoPublicKey, public_nonce: &RistrettoPublicKey, input_revealed_amount: &Amount, output_revealed_amount: &Amount, -) -> [u8; 64] { +) -> Hash64 { engine_hasher64(EngineHashDomainLabel::ConfidentialTransfer) .chain(excess) .chain(public_nonce) .chain(input_revealed_amount) .chain(output_revealed_amount) .result() + .into() } pub fn viewable_balance_proof64( commitment: &PedersenCommitment, view_key: &RistrettoPublicKey, challenge_fields: ViewableBalanceProofChallengeFields<'_>, -) -> [u8; 64] { +) -> Hash64 { engine_hasher64(EngineHashDomainLabel::ViewableBalanceProof) .chain(commitment) .chain(view_key) .chain(&challenge_fields) .result() + .into() } pub fn stealth_balance_proof64( @@ -41,28 +46,40 @@ pub fn stealth_balance_proof64( public_nonce: &RistrettoPublicKey, stealth_inputs_statement: &StealthInputsStatement, stealth_outputs_statement: &StealthOutputsStatement, -) -> [u8; 64] { +) -> Hash64 { engine_hasher64(EngineHashDomainLabel::StealthBalanceProof) .chain(public_excess) .chain(public_nonce) .chain(stealth_inputs_statement) .chain(stealth_outputs_statement) .result() + .into() } pub fn stealth_ownership64( commitment: &PedersenCommitmentBytes, public_output_nonce: &RistrettoPublicKeyBytes, -) -> [u8; 64] { + metadata_hash: &Hash64, +) -> Hash64 { engine_hasher64(EngineHashDomainLabel::StealthOwnership) .chain(commitment) .chain(public_output_nonce) + .chain(metadata_hash) + .result() + .into() +} + +pub fn stealth_statement_metadata64(outputs_statement: &StealthOutputsStatement) -> Hash64 { + engine_hasher64(EngineHashDomainLabel::StealthOwnership) + .chain(outputs_statement) .result() + .into() } -pub fn value_proof_message(commitment: &PedersenCommitmentBytes, value: &Amount) -> [u8; 64] { +pub fn value_proof_message(commitment: &PedersenCommitmentBytes, value: &Amount) -> Hash64 { engine_hasher64(EngineHashDomainLabel::ValueProof) .chain(commitment) .chain(value) .result() + .into() } diff --git a/crates/engine_types/src/crypto/mod.rs b/crates/engine_types/src/crypto/mod.rs index 1bc6e72284..66d7a36566 100644 --- a/crates/engine_types/src/crypto/mod.rs +++ b/crates/engine_types/src/crypto/mod.rs @@ -6,11 +6,9 @@ mod helpers; pub mod messages; mod output; pub mod range_proof; -mod utxo_spend; mod value_lookup_table; pub use elgamal::*; pub use helpers::*; pub use output::*; -pub use utxo_spend::*; pub use value_lookup_table::*; diff --git a/crates/engine_types/src/crypto/utxo_spend.rs b/crates/engine_types/src/crypto/utxo_spend.rs deleted file mode 100644 index 839b892504..0000000000 --- a/crates/engine_types/src/crypto/utxo_spend.rs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2025 The Tari Project -// SPDX-License-Identifier: BSD-3-Clause - -use tari_crypto::ristretto::RistrettoPublicKey; -use tari_template_lib::models::StealthInput; - -use crate::{ - crypto::{messages, try_decode_to_signature}, - resource_container::ResourceError, - ConvertFromByteType, - UtxoOutput, -}; - -pub fn verify_utxo_spend_permission(utxo: &UtxoOutput, input: &StealthInput) -> Result<(), ResourceError> { - if input.owner_proof.public_nonce().is_zero() { - return Err(ResourceError::InvalidSpend { - details: "Ownership proof public nonce cannot be zero".to_string(), - }); - } - - let balance_proof = try_decode_to_signature(&input.owner_proof).ok_or_else(|| ResourceError::InvalidSpend { - details: "Malformed ownership proof".to_string(), - })?; - - let message = messages::stealth_ownership64(&input.commitment, &utxo.output.public_nonce); - let signer_pk = RistrettoPublicKey::convert_from_byte_type(&utxo.owner_public_key).map_err(|_| { - ResourceError::InvalidSpend { - details: "Non-canonical compressed owner public key".to_string(), - } - })?; - - if !balance_proof.verify(&signer_pk, message) { - return Err(ResourceError::InvalidSpend { - details: format!("Invalid ownership proof for input with commitment {}", input.commitment), - }); - } - - Ok(()) -} diff --git a/crates/engine_types/src/hash.rs b/crates/engine_types/src/hash.rs new file mode 100644 index 0000000000..74fb8e5c44 --- /dev/null +++ b/crates/engine_types/src/hash.rs @@ -0,0 +1,183 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::{ + fmt, + fmt::{Display, Formatter}, + ops::{Deref, DerefMut}, + str::FromStr, +}; + +use serde::{Deserialize, Serialize}; + +use crate::serde_with; + +/// Representation of a 32-byte hash value +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize, borsh::BorshSerialize)] +#[serde(transparent)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +pub struct Hash64( + #[serde(with = "serde_with::hex")] + #[cfg_attr(feature = "ts", ts(type = "string"))] + [u8; Self::LENGTH], +); + +impl Hash64 { + pub const LENGTH: usize = 64; + + pub const fn zero() -> Self { + Self::from_array([0u8; Self::LENGTH]) + } + + pub const fn from_array(bytes: [u8; Self::LENGTH]) -> Self { + Self(bytes) + } + + pub const fn into_array(self) -> [u8; Self::LENGTH] { + self.0 + } + + pub const fn as_slice(&self) -> &[u8] { + &self.0 + } + + pub fn from_hex(s: &str) -> Result { + if s.len() != Self::LENGTH * 2 { + return Err(HashParseError::InvalidLength); + } + let mut buf = [0u8; Self::LENGTH]; + hex::decode_to_slice(s, &mut buf)?; + Ok(Hash64(buf)) + } + + pub fn write_hex_fmt(&self, writer: &mut W) -> fmt::Result { + for b in self.0 { + write!(writer, "{:02x?}", b)?; + } + Ok(()) + } + + pub fn try_from_slice(data: &[u8]) -> Result { + Self::try_from(data) + } + + /// Returns the leading `N` bytes of the hash + /// + /// # Panics + /// + /// Panics if `N` is greater than Self::LENGTH (32) + pub fn leading_bytes(&self) -> [u8; N] { + self.0 + .get(..N) + .expect("invariant violation: N > Self::LENGTH") + .try_into() + .unwrap() + } + + /// Returns the trailing `N` bytes of the hash + /// + /// # Panics + /// + /// Panics if `N` is greater than Self::LENGTH (32) + pub fn trailing_bytes(&self) -> [u8; N] { + self.0 + .get((Self::LENGTH - N)..Self::LENGTH) + .expect("invariant violation: N > Self::LENGTH") + .try_into() + .unwrap() + } +} + +impl AsRef<[u8]> for Hash64 { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From<[u8; Self::LENGTH]> for Hash64 { + fn from(hash: [u8; Self::LENGTH]) -> Self { + Self::from_array(hash) + } +} + +impl FromStr for Hash64 { + type Err = HashParseError; + + fn from_str(s: &str) -> Result { + Hash64::from_hex(s) + } +} + +impl TryFrom<&[u8]> for Hash64 { + type Error = HashParseError; + + fn try_from(value: &[u8]) -> Result { + if value.len() != Self::LENGTH { + return Err(HashParseError::InvalidLength); + } + let mut hash = [0u8; Self::LENGTH]; + hash.copy_from_slice(value); + Ok(Hash64::from_array(hash)) + } +} + +impl TryFrom> for Hash64 { + type Error = HashParseError; + + fn try_from(value: Vec) -> Result { + Hash64::try_from(value.as_slice()) + } +} + +impl Deref for Hash64 { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Hash64 { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Default for Hash64 { + fn default() -> Self { + Self::zero() + } +} + +impl Display for Hash64 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for x in self.0 { + write!(f, "{:02x?}", x)?; + } + Ok(()) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum HashParseError { + #[error("Invalid hex string: {0}")] + Hex(#[from] hex::FromHexError), + #[error("Invalid length")] + InvalidLength, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize_deserialize() { + let hash = Hash64::default(); + let mut buf = Vec::new(); + tari_bor::encode_into_std_writer(&hash, &mut buf).unwrap(); + let val = tari_bor::to_value(&hash).unwrap(); + assert_eq!(val, tari_bor::Value::Bytes(vec![0u8; Hash64::LENGTH])); + let hash2 = tari_bor::decode(&buf).unwrap(); + assert_eq!(hash, hash2); + } +} diff --git a/crates/engine_types/src/lib.rs b/crates/engine_types/src/lib.rs index 6b521cef3e..37e9ead456 100644 --- a/crates/engine_types/src/lib.rs +++ b/crates/engine_types/src/lib.rs @@ -32,6 +32,7 @@ pub mod entity_id_provider; pub mod id_provider; mod borsh; +mod hash; pub mod json_cbor; pub mod published_template; mod substate_serde; @@ -39,6 +40,7 @@ mod utxo; mod validator_fee; pub use byte_types::*; +pub use hash::*; pub use template::{calculate_template_binary_hash, parse_template_address}; pub use utxo::*; pub use validator_fee::*; diff --git a/crates/engine_types/src/stealth/transfer.rs b/crates/engine_types/src/stealth/transfer.rs index 0c17bf2fe8..ed7460b1c8 100644 --- a/crates/engine_types/src/stealth/transfer.rs +++ b/crates/engine_types/src/stealth/transfer.rs @@ -3,11 +3,13 @@ use log::*; use tari_crypto::{ - keys::PublicKey, ristretto::{pedersen::PedersenCommitment, RistrettoPublicKey, RistrettoSecretKey}, - tari_utilities::ByteArrayError, + tari_utilities::{ByteArray, ByteArrayError}, +}; +use tari_template_lib::{ + models::{StealthInput, StealthTransferStatement}, + types::Amount, }; -use tari_template_lib::{models::StealthTransferStatement, types::Amount}; use crate::{ crypto::{commit_amount_checked, messages, try_decode_to_signature}, @@ -15,6 +17,8 @@ use crate::{ stealth, stealth::ValidatedStealthOutput, ConvertFromByteType, + Hash64, + UtxoOutput, }; const LOG_TARGET: &str = "tari::engine_types::stealth::transfer"; @@ -25,17 +29,12 @@ pub struct ValidatedStealthTransfer { pub revealed_output_amount: Amount, } -pub fn validate_transfer( +pub fn validate_transfer_balance( transfer: &StealthTransferStatement, view_key: Option<&RistrettoPublicKey>, ) -> Result { basic_validations(transfer)?; let validated_outputs = stealth::validate_stealth_outputs_statement(&transfer.outputs_statement, view_key)?; - if transfer.balance_proof.public_nonce().is_zero() { - return Err(ResourceError::InvalidBalanceProof { - details: "Balance proof public nonce cannot be zero".to_string(), - }); - } let balance_proof = try_decode_to_signature(&transfer.balance_proof).ok_or_else(|| ResourceError::InvalidBalanceProof { @@ -51,10 +50,9 @@ pub fn validate_transfer( }); } - // The public excess is 0 (s = r + e.0) so we check s.G ?= r.G + e.0.G - NOTE: normal signature verification - // explicitly rejects the zero key however since we've checked that the revealed amounts are equal and - // there are no inputs or outputs (basic_validations), we consider this a valid balance proof. - if RistrettoPublicKey::from_secret_key(balance_proof.get_signature()) != *balance_proof.get_public_nonce() { + // In this case, the public excess is 0 (s = r + e.0 = r) which leaks the secret nonce. So instead, we enforce + // that a zero signature MUST be used for this case. + if balance_proof.get_signature().as_bytes() != RistrettoSecretKey::default().as_bytes() { return Err(ResourceError::InvalidBalanceProof { details: "Balance proof signature verification failed for revealed amount. This typically indicates \ that the transfer statement total input amount != total output amount." @@ -134,6 +132,37 @@ pub fn validate_transfer( }) } +pub fn validate_ownership_proof( + utxo: &UtxoOutput, + input: &StealthInput, + metadata_hash: &Hash64, +) -> Result<(), ResourceError> { + if input.owner_proof.public_nonce().is_zero() { + return Err(ResourceError::InvalidSpend { + details: "Ownership proof public nonce cannot be zero".to_string(), + }); + } + + let owner_proof = try_decode_to_signature(&input.owner_proof).ok_or_else(|| ResourceError::InvalidSpend { + details: "Malformed ownership proof".to_string(), + })?; + + let signer_pk = RistrettoPublicKey::convert_from_byte_type(&utxo.owner_public_key).map_err(|_| { + ResourceError::InvalidSpend { + details: "Non-canonical compressed owner public key".to_string(), + } + })?; + + let message = messages::stealth_ownership64(&input.commitment, &utxo.output.public_nonce, metadata_hash); + if !owner_proof.verify(&signer_pk, message) { + return Err(ResourceError::InvalidSpend { + details: format!("Invalid ownership proof for input with commitment {}", input.commitment), + }); + } + + Ok(()) +} + fn basic_validations(transfer: &StealthTransferStatement) -> Result<(), ResourceError> { if transfer.inputs_statement.revealed_amount.is_negative() { return Err(ResourceError::InvalidBalanceProof { @@ -158,17 +187,24 @@ fn basic_validations(transfer: &StealthTransferStatement) -> Result<(), Resource }); } - if transfer.inputs_statement.inputs.is_empty() && - transfer.outputs_statement.outputs.is_empty() && - transfer.inputs_statement.revealed_amount != transfer.outputs_statement.revealed_output_amount - { + // Check the balance if there are no stealth inputs or outputs. Since the excess will be zero in this case, the + // balance signature (r + 0.e) does not prove the balance. + if transfer.inputs_statement.inputs.is_empty() && transfer.outputs_statement.outputs.is_empty() { + if transfer.inputs_statement.revealed_amount != transfer.outputs_statement.revealed_output_amount { + return Err(ResourceError::InvalidBalanceProof { + details: format!( + "Revealed input amount {} does not match revealed output amount {} - no stealth inputs or outputs \ + provided", + transfer.inputs_statement.revealed_amount, transfer.outputs_statement.revealed_output_amount + ), + }); + } + } else if transfer.balance_proof.public_nonce().is_zero() || transfer.balance_proof.signature().is_zero() { return Err(ResourceError::InvalidBalanceProof { - details: format!( - "Revealed input amount {} does not match revealed output amount {} - no stealth inputs or outputs \ - provided", - transfer.inputs_statement.revealed_amount, transfer.outputs_statement.revealed_output_amount - ), + details: "Balance proof public nonce and signature cannot be zero".to_string(), }); + } else { + // Ok } Ok(()) diff --git a/crates/template_lib_types/src/crypto/scalar.rs b/crates/template_lib_types/src/crypto/scalar.rs index a301607d64..365b9af598 100644 --- a/crates/template_lib_types/src/crypto/scalar.rs +++ b/crates/template_lib_types/src/crypto/scalar.rs @@ -25,6 +25,10 @@ impl Scalar32Bytes { Self([0u8; Self::length()]) } + pub fn is_zero(&self) -> bool { + self.0.iter().all(|&b| b == 0) + } + pub fn from_bytes(bytes: &[u8]) -> Result { if bytes.len() != Self::length() { return Err(InvalidByteLengthError { diff --git a/crates/template_lib_types/src/encrypted_data.rs b/crates/template_lib_types/src/encrypted_data.rs index 03c486f223..a0e189a4e8 100644 --- a/crates/template_lib_types/src/encrypted_data.rs +++ b/crates/template_lib_types/src/encrypted_data.rs @@ -23,6 +23,10 @@ impl EncryptedData { pub const SIZE_TAG: usize = 16; pub const SIZE_VALUE: usize = size_of::(); + pub fn empty() -> Self { + Self(MaxBytes::empty()) + } + pub const fn min_size() -> usize { Self::ENCRYPTED_DATA_SIZE_WITHOUT_MEMO } diff --git a/crates/template_lib_types/src/max_bytes.rs b/crates/template_lib_types/src/max_bytes.rs index 7c284e88d1..7c6d738635 100644 --- a/crates/template_lib_types/src/max_bytes.rs +++ b/crates/template_lib_types/src/max_bytes.rs @@ -35,6 +35,10 @@ impl MaxBytes { pub fn into_vec(self) -> Vec { self.bytes.into_vec() } + + pub fn empty() -> Self { + Self { bytes: Box::new([]) } + } } impl AsRef<[u8]> for MaxBytes { @@ -52,9 +56,7 @@ impl DerefMut for MaxBytes { impl Default for MaxBytes { fn default() -> Self { - Self { - bytes: vec![0u8; N].into_boxed_slice(), - } + Self::empty() } } diff --git a/crates/transaction/src/builder/mod.rs b/crates/transaction/src/builder/mod.rs index 8b77061a77..97096fd356 100644 --- a/crates/transaction/src/builder/mod.rs +++ b/crates/transaction/src/builder/mod.rs @@ -66,7 +66,11 @@ impl TransactionBuilder { } } - pub fn then T, T>(self, f: F) -> T { + pub fn then Self>(self, f: F) -> Self { + f(self) + } + + pub fn map T, T>(self, f: F) -> T { f(self) } diff --git a/crates/wallet/crypto/src/balance_proof.rs b/crates/wallet/crypto/src/balance_proof.rs index 5988115867..a5de0b90dc 100644 --- a/crates/wallet/crypto/src/balance_proof.rs +++ b/crates/wallet/crypto/src/balance_proof.rs @@ -6,7 +6,7 @@ use tari_crypto::{ keys::PublicKey, ristretto::{RistrettoPublicKey, RistrettoSecretKey}, }; -use tari_engine_types::{crypto::messages, hashing::EngineSchnorrSignature, ToByteType}; +use tari_engine_types::{crypto::messages, hashing::EngineSchnorrSignature, Hash64, ToByteType}; use tari_template_lib::{ models::{StealthInputsStatement, StealthOutputsStatement}, prelude::{Amount, BalanceProofSignature, PedersenCommitmentBytes, RistrettoPublicKeyBytes, SchnorrSignatureBytes}, @@ -35,13 +35,17 @@ pub(crate) fn generate_confidential_balance_proof( sig.to_byte_type() } -pub(crate) fn generate_stealth_balance_proof_signature( +pub fn generate_stealth_balance_proof_signature( agg_input_mask: &RistrettoSecretKey, agg_output_mask: &RistrettoSecretKey, inputs_statement: &StealthInputsStatement, outputs_statement: &StealthOutputsStatement, ) -> BalanceProofSignature { let secret_excess = agg_input_mask - agg_output_mask; + if secret_excess == RistrettoSecretKey::default() { + // This is a revealed only proof + return BalanceProofSignature::zero(); + } let public_excess = RistrettoPublicKey::from_secret_key(&secret_excess); let (nonce, public_nonce) = RistrettoPublicKey::random_keypair(&mut OsRng); let message = messages::stealth_balance_proof64(&public_excess, &public_nonce, inputs_statement, outputs_statement); @@ -54,8 +58,9 @@ pub(crate) fn generate_stealth_owner_proof_signature( secret_key: &RistrettoSecretKey, public_output_nonce: &RistrettoPublicKeyBytes, commitment: &PedersenCommitmentBytes, + metadata_hash: &Hash64, ) -> SchnorrSignatureBytes { - let message = messages::stealth_ownership64(commitment, public_output_nonce); + let message = messages::stealth_ownership64(commitment, public_output_nonce, metadata_hash); let sig = EngineSchnorrSignature::sign(secret_key, message, &mut OsRng).unwrap(); sig.to_byte_type() } diff --git a/crates/wallet/crypto/src/stealth.rs b/crates/wallet/crypto/src/stealth.rs index dcff123c53..ca119bb176 100644 --- a/crates/wallet/crypto/src/stealth.rs +++ b/crates/wallet/crypto/src/stealth.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_crypto::ristretto::RistrettoSecretKey; -use tari_engine_types::ToByteType; +use tari_engine_types::{crypto::messages, ToByteType}; use tari_template_lib::{ models::{ StealthInput, @@ -53,6 +53,10 @@ where let (lower, upper) = input_iter.size_hint(); upper.unwrap_or(lower) }; + + let outputs_statement = create_outputs_statement(output_statements.clone(), revealed_output_amount)?; + let outputs_statement_hash = messages::stealth_statement_metadata64(&outputs_statement); + let (inputs_to_spend, agg_input_mask) = input_iter.try_fold( (Vec::with_capacity(num_inputs_estimate), RistrettoSecretKey::default()), |(mut inputs, agg_input), input| { @@ -69,6 +73,7 @@ where &input.owner_secret, &input.public_nonce.to_byte_type(), &commitment.to_byte_type(), + &outputs_statement_hash, ); inputs.push(StealthInput { commitment: commitment.to_byte_type(), @@ -79,7 +84,6 @@ where )?; let agg_output_mask = output_statements - .clone() .into_iter() .map(|stmt| &stmt.witness.mask) .fold(RistrettoSecretKey::default(), |agg, mask| agg + mask); @@ -88,7 +92,6 @@ where inputs: inputs_to_spend.clone(), revealed_amount: revealed_input_amount, }; - let outputs_statement = create_outputs_statement(output_statements, revealed_output_amount)?; let balance_proof = generate_stealth_balance_proof_signature( &agg_input_mask, diff --git a/crates/wallet/crypto/tests/output_statement.rs b/crates/wallet/crypto/tests/stealth_transfer_statement.rs similarity index 56% rename from crates/wallet/crypto/tests/output_statement.rs rename to crates/wallet/crypto/tests/stealth_transfer_statement.rs index 0947c4bbd4..13afb49059 100644 --- a/crates/wallet/crypto/tests/output_statement.rs +++ b/crates/wallet/crypto/tests/stealth_transfer_statement.rs @@ -6,17 +6,24 @@ use tari_crypto::{ keys::{PublicKey, SecretKey}, ristretto::{RistrettoPublicKey, RistrettoSecretKey}, }; -use tari_engine_types::stealth; +use tari_engine_types::{ + crypto::{messages, PrivateOutput}, + resource_container::ResourceError, + stealth, + ToByteType, + UtxoOutput, +}; use tari_ootle_common_types::crypto::create_key_pair_from_seed; use tari_ootle_wallet_crypto::{ + balance_proof::generate_stealth_balance_proof_signature, confidential, - stealth::create_transfer_statement, + stealth::{create_outputs_statement, create_transfer_statement}, MaskAndValue, UnblindedOutputWitness, UnblindedStealthInputWitness, UnblindedStealthOutputWitness, }; -use tari_template_lib::types::Amount; +use tari_template_lib::types::{crypto::UtxoTag, Amount, EncryptedData}; #[test] fn it_create_a_valid_revealed_only_proof() { @@ -28,14 +35,12 @@ fn it_create_a_valid_revealed_only_proof() { } mod stealth_tests { - use tari_template_lib::types::{crypto::UtxoTag, EncryptedData}; - use super::*; #[test] fn it_errors_for_noop_transfer() { let statement = create_transfer_statement(&[], Amount::zero(), &[], Amount::zero()).unwrap(); - stealth::validate_transfer(&statement, None).unwrap_err(); + stealth::validate_transfer_balance(&statement, None).unwrap_err(); } #[test] @@ -54,7 +59,7 @@ mod stealth_tests { ) .unwrap(); - stealth::validate_transfer(&statement, None).unwrap(); + stealth::validate_transfer_balance(&statement, None).unwrap(); } #[test] @@ -73,7 +78,7 @@ mod stealth_tests { ) .unwrap(); - stealth::validate_transfer(&statement, None).unwrap(); + stealth::validate_transfer_balance(&statement, None).unwrap(); } #[test] @@ -81,12 +86,76 @@ mod stealth_tests { let revealed_input_amount = Amount::from(6000); let revealed_output_amount = Amount::from(6000); let statement = create_transfer_statement(&[], revealed_input_amount, &[], revealed_output_amount).unwrap(); - stealth::validate_transfer(&statement, None).unwrap(); + stealth::validate_transfer_balance(&statement, None).unwrap(); let revealed_input_amount = Amount::from(6000); let revealed_output_amount = Amount::from(5999); let statement = create_transfer_statement(&[], revealed_input_amount, &[], revealed_output_amount).unwrap(); - stealth::validate_transfer(&statement, None).unwrap_err(); // Invalid, output is less than input + stealth::validate_transfer_balance(&statement, None).unwrap_err(); // Invalid, output is less than input + } + + #[test] + fn it_fails_to_validate_if_outputs_are_replaced() { + let inputs = make_input_statements(&[(1, 1000), (2, 2000), (3, 3000)]); + let revealed_input_amount = Amount::from(6000); + + let output_statements = make_output_statements(&[100, 200, 300]); + let revealed_output_amount = Amount::from(6000 + 6000 - 100 - 200 - 300); + + let mut statement = create_transfer_statement( + &inputs, + revealed_input_amount, + &output_statements, + revealed_output_amount, + ) + .unwrap(); + + // Make new output statements + let output_statements = make_output_statements(&[100, 200, 300]); + + // Recreate the output statement and balance proof + let agg_output_mask = output_statements + .iter() + .map(|stmt| &stmt.witness.mask) + .fold(RistrettoSecretKey::default(), |agg, mask| agg + mask); + + let agg_input_mask = inputs + .iter() + .map(|stmt| &stmt.mask_and_value.mask) + .fold(RistrettoSecretKey::default(), |agg, mask| agg + mask); + + statement.outputs_statement = create_outputs_statement(&output_statements, revealed_output_amount).unwrap(); + + statement.balance_proof = generate_stealth_balance_proof_signature( + &agg_input_mask, + &agg_output_mask, + &statement.inputs_statement, + &statement.outputs_statement, + ); + + // This passes because we recreated the balance proof correctly + stealth::validate_transfer_balance(&statement, None).unwrap(); + + let metadata_hash = messages::stealth_statement_metadata64(&statement.outputs_statement); + for (i, input) in statement.inputs_statement.inputs.iter().enumerate() { + let original_input = &inputs.get(i).expect("input index out of range"); + // Convert the input into a UTXO to spend + let utxo = UtxoOutput { + output: PrivateOutput { + public_nonce: original_input.public_nonce.to_byte_type(), + // Encrypted data is not needed for ownership proof validation + encrypted_data: EncryptedData::empty(), + minimum_value_promise: 0, + viewable_balance: None, + }, + owner_public_key: RistrettoPublicKey::from_secret_key(&original_input.owner_secret).to_byte_type(), + tag: UtxoTag::new(0), + }; + + // This fails because the outputs have been malleated + let err = stealth::validate_ownership_proof(&utxo, input, &metadata_hash).unwrap_err(); + assert!(matches!(err, ResourceError::InvalidSpend { .. })); + } } fn make_input_statements(amounts: &[(u8, u64)]) -> Vec { diff --git a/crates/wallet/sdk/src/apis/key_manager.rs b/crates/wallet/sdk/src/apis/key_manager.rs index 7f0f5f2a6f..a2baaf91b2 100644 --- a/crates/wallet/sdk/src/apis/key_manager.rs +++ b/crates/wallet/sdk/src/apis/key_manager.rs @@ -13,12 +13,13 @@ use tari_ootle_common_types::{ optional::{IsNotFoundError, Optional}, Network, }; -use tari_ootle_wallet_crypto::encryption::{decrypt_with_password, encrypt_with_password}; -use tari_transaction_components::{key_manager, key_manager::tari_key_manager::TariKeyManager}; +use tari_ootle_wallet_crypto::encryption::encrypt_with_password; +use tari_transaction_components::key_manager::tari_key_manager::TariKeyManager; use crate::{ apis::password_manager::{PasswordManagerApi, PasswordManagerApiError}, - cipher_seed::WalletCipherSeed, + key_managers::WalletKeyStore, + local_key_store::LocalKeyStore, models::{ DerivedKeyIndex, DerivedKeyPair, @@ -42,7 +43,7 @@ pub type WalletKeyManager = TariKeyManager>; pub struct KeyManagerApi<'a, TStore> { network: Network, store: &'a TStore, - cipher_seed: &'a WalletCipherSeed, + key_store: LocalKeyStore<'a, TStore>, password_manager: PasswordManagerApi<'a, TStore>, } @@ -50,13 +51,13 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> { pub(crate) fn new( network: Network, store: &'a TStore, - cipher_seed: &'a WalletCipherSeed, + key_store: LocalKeyStore<'a, TStore>, password_manager: PasswordManagerApi<'a, TStore>, ) -> Self { Self { network, store, - cipher_seed, + key_store, password_manager, } } @@ -64,16 +65,17 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> { pub fn get_all_derived_keys(&self, branch: KeyBranch) -> Result, KeyManagerApiError> { let all_keys = self.store.with_read_tx(|tx| tx.key_manager_get_all(branch.as_str()))?; let mut keys = Vec::with_capacity(all_keys.len()); - let km = self.get_key_manager(branch.as_str())?; + for (index, active) in all_keys { - let key = km - .derive_key(index) - .map_err(key_manager::error::KeyManagerServiceError::from)?; - let pk = RistrettoPublicKey::from_secret_key(&key.key); + let key = self + .key_store + .derive_secret(branch.as_str(), index) + .map_err(|e| KeyManagerApiError::KeyStoreError { source: e.into() })?; + let pk = RistrettoPublicKey::from_secret_key(&key); keys.push(WalletKeyRecord { key_id: KeyId::derived(index), public_key: pk, - secret_key: key.key, + secret_key: key, is_active: active, }); } @@ -81,27 +83,11 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> { } pub fn get_imported_key(&self, id: ImportedKeyId) -> Result { - let password = self.password_manager.get_cipher_seed_password()?; - self.store.with_read_tx(|tx| { - let (key_type, encrypted_key) = tx.key_manager_get_raw_imported_key(id)?; - let decrypted = decrypt_with_password(&encrypted_key, password.reveal()).map_err(|e| { - KeyManagerApiError::StoreError(WalletStorageError::DecryptionError { - operation: "KeyManagerApi::get_imported_key", - details: format!("Failed to decrypt imported key: {}", e), - }) - })?; - Ok(ImportedWalletKey { - key: RistrettoSecretKey::from_canonical_bytes(&decrypted).map_err(|_| { - KeyManagerApiError::StoreError(WalletStorageError::DecodingError { - operation: "KeyManagerApi::get_imported_key", - item: "imported_key", - details: "Failed to decode imported key".to_string(), - }) - })?, - import_id: id, - key_type, - }) - }) + let key = self + .key_store + .get_imported_secret(id) + .map_err(|e| KeyManagerApiError::KeyStoreError { source: e.into() })?; + Ok(ImportedWalletKey { key, import_id: id }) } pub fn import_key( @@ -176,11 +162,14 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> { branch: KeyBranch, index: DerivedKeyIndex, ) -> Result { - let km = self.get_key_manager(branch)?; - let key = km - .derive_key(index) - .expect("derive_key only panics if the hasher does not produce 32 bytes"); - Ok(key.into()) + let secret = self + .key_store + .derive_secret(branch.as_str(), index) + .map_err(|e| KeyManagerApiError::KeyStoreError { source: e.into() })?; + Ok(DerivedWalletKey { + key: secret, + key_index: index, + }) } pub fn derive_keypair( @@ -328,25 +317,16 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> { }, } } - - fn get_key_manager>(&self, branch: B) -> Result { - let cipher_seed = self.cipher_seed.cipher_seed().ok_or(KeyManagerApiError::ReadOnlyMode)?; - // We dont ever use the index in the key manager i.e. we dont ever call next_key on it, instead we always use - // derive_key - Ok(WalletKeyManager::from( - cipher_seed.clone(), - branch.as_ref().to_string(), - 0, - )) - } } #[derive(Debug, thiserror::Error)] pub enum KeyManagerApiError { #[error("Store error: {0}")] StoreError(#[from] WalletStorageError), - #[error("Key manager error: {0}")] - KeyManagerError(#[from] key_manager::error::KeyManagerServiceError), + #[error("Key store error: {source}")] + KeyStoreError { + source: Box, + }, #[error("Key {key_id} not found")] KeyNotFound { key_id: KeyId }, #[error("Password manager error: {0}")] diff --git a/crates/wallet/sdk/src/key_managers/backend.rs b/crates/wallet/sdk/src/key_managers/backend.rs index c625ff9092..b2009a3008 100644 --- a/crates/wallet/sdk/src/key_managers/backend.rs +++ b/crates/wallet/sdk/src/key_managers/backend.rs @@ -3,7 +3,7 @@ use tari_crypto::ristretto::{RistrettoPublicKey, RistrettoSchnorr, RistrettoSecretKey}; -use crate::models::KeyId; +use crate::models::{DerivedKeyIndex, KeyId}; pub struct SignatureOutput { pub signature: RistrettoSchnorr, @@ -19,5 +19,7 @@ pub trait KeyManagerBackend { pub trait WalletKeyStore { type Error; + fn derive_secret(&self, branch: &str, key_index: DerivedKeyIndex) -> Result; + fn get_imported_secret(&self, key: K) -> Result; } diff --git a/crates/wallet/sdk/src/key_managers/local.rs b/crates/wallet/sdk/src/key_managers/local.rs index 8a39350815..611530b9df 100644 --- a/crates/wallet/sdk/src/key_managers/local.rs +++ b/crates/wallet/sdk/src/key_managers/local.rs @@ -1,15 +1,12 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use blake2::Blake2b; -use digest::{consts::U64, crypto_common::rand_core::OsRng}; -use tari_common_types::seeds::cipher_seed::CipherSeed; +use digest::crypto_common::rand_core::OsRng; use tari_crypto::{ keys::PublicKey, ristretto::{RistrettoPublicKey, RistrettoSchnorr}, }; use tari_ootle_common_types::optional::IsNotFoundError; -use tari_transaction_components::key_manager::tari_key_manager::TariKeyManager; use crate::{ apis::password_manager::PasswordManagerApiError, @@ -18,26 +15,18 @@ use crate::{ storage::WalletStorageError, }; -type WalletKeyManager = TariKeyManager>; - #[derive(Debug, Clone)] -pub struct LocalKeyManager<'a, TKeyStore> { - cipher_seed: &'a CipherSeed, +pub struct LocalKeyManager { key_store: TKeyStore, } -impl<'a, TKeyStore: WalletKeyStore> LocalKeyManager<'a, TKeyStore> { - pub fn new(cipher_seed: &'a CipherSeed, key_store: TKeyStore) -> Self { - Self { cipher_seed, key_store } - } - - /// WARNING: dont use next_key on the key manager because this will always return the same key - fn get_key_manager(&mut self, branch: &str) -> WalletKeyManager { - WalletKeyManager::from(self.cipher_seed.clone(), branch.to_string(), 0) +impl> LocalKeyManager { + pub fn new(key_store: TKeyStore) -> Self { + Self { key_store } } } -impl KeyManagerBackend for LocalKeyManager<'_, TKeyStore> +impl KeyManagerBackend for LocalKeyManager where M: AsRef<[u8]>, TKeyStore: WalletKeyStore, @@ -46,13 +35,10 @@ where fn try_sign(&mut self, branch: &str, key_id: KeyId, message: M) -> Result { let secret = match key_id { - KeyId::Derived { index } => { - let km = self.get_key_manager(branch); - let key = km - .derive_key(index) - .expect("BUG: Key derivation is infallible because it internally hashes to a canonical form."); - key.key - }, + KeyId::Derived { index } => self + .key_store + .derive_secret(branch, index) + .map_err(LocalKeyManagerError::KeyStoreError)?, KeyId::Imported { local_key_id } => self .key_store .get_imported_secret(local_key_id) diff --git a/crates/wallet/sdk/src/local_key_store.rs b/crates/wallet/sdk/src/local_key_store.rs index c68ef73bcd..5ab93e2369 100644 --- a/crates/wallet/sdk/src/local_key_store.rs +++ b/crates/wallet/sdk/src/local_key_store.rs @@ -1,34 +1,58 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause +use tari_common_types::seeds::cipher_seed::CipherSeed; use tari_crypto::{ristretto::RistrettoSecretKey, tari_utilities::ByteArray}; use tari_ootle_wallet_crypto::encryption::{decrypt_with_password, CipherError}; use crate::{ - apis::password_manager::{PasswordManagerApi, PasswordManagerApiError}, + apis::{ + key_manager::WalletKeyManager, + password_manager::{PasswordManagerApi, PasswordManagerApiError}, + }, + cipher_seed::WalletCipherSeed, key_managers::WalletKeyStore, - models::ImportedKeyId, + models::{DerivedKeyIndex, ImportedKeyId}, storage::{WalletStorageError, WalletStore, WalletStoreReader}, }; #[derive(Clone)] pub struct LocalKeyStore<'a, TStore> { password_manager_api: PasswordManagerApi<'a, TStore>, + cipher_seed: &'a WalletCipherSeed, wallet_store: &'a TStore, } impl<'a, TStore> LocalKeyStore<'a, TStore> { - pub fn new(password_manager_api: PasswordManagerApi<'a, TStore>, wallet_store: &'a TStore) -> Self { + pub fn new( + cipher_seed: &'a WalletCipherSeed, + password_manager_api: PasswordManagerApi<'a, TStore>, + wallet_store: &'a TStore, + ) -> Self { Self { + cipher_seed, password_manager_api, wallet_store, } } + + fn get_cipher_seed(&self) -> Result<&CipherSeed, LocalKeyStoreError> { + self.cipher_seed.cipher_seed().ok_or(LocalKeyStoreError::NoCipherSeed) + } } impl WalletKeyStore for LocalKeyStore<'_, TStore> { type Error = LocalKeyStoreError; + fn derive_secret(&self, branch: &str, key_index: DerivedKeyIndex) -> Result { + let cipher_seed = self.get_cipher_seed()?; + let km = WalletKeyManager::from(cipher_seed.clone(), branch.to_string(), 0); + let secret = km + .derive_key(key_index) + .expect("Key derivation bug: derive key internally creates a canonical key and must not fail"); + Ok(secret.key) + } + fn get_imported_secret(&self, key: ImportedKeyId) -> Result { let password = self.password_manager_api.get_cipher_seed_password()?; let (_ty, encrypted) = self @@ -54,4 +78,6 @@ pub enum LocalKeyStoreError { WalletStorage(#[from] WalletStorageError), #[error("Cipher error: {0}")] Cipher(#[from] CipherError), + #[error("Cannot derive keys because no cipher seed was provided")] + NoCipherSeed, } diff --git a/crates/wallet/sdk/src/models/key.rs b/crates/wallet/sdk/src/models/key.rs index 1f7a5093f4..174186fa46 100644 --- a/crates/wallet/sdk/src/models/key.rs +++ b/crates/wallet/sdk/src/models/key.rs @@ -86,7 +86,6 @@ pub struct WalletOotleAddressWithKeyIds { pub struct ImportedWalletKey { pub key: RistrettoSecretKey, pub import_id: ImportedKeyId, - pub key_type: KeyType, } impl ImportedWalletKey { diff --git a/crates/wallet/sdk/src/sdk.rs b/crates/wallet/sdk/src/sdk.rs index 969abf98df..0ef72bc158 100644 --- a/crates/wallet/sdk/src/sdk.rs +++ b/crates/wallet/sdk/src/sdk.rs @@ -45,7 +45,7 @@ use crate::{ const LOG_TARGET: &str = "wallet::sdk::api"; -pub type LocalSignerApi<'a, TStore> = SignerApi>>; +pub type LocalSignerApi<'a, TStore> = SignerApi>>; #[derive(Debug, Clone)] pub struct WalletSdkConfig { @@ -161,26 +161,16 @@ where KeyManagerApi::new( network, &self.store, - &self.loaded_cipher_seed, + LocalKeyStore::new(&self.loaded_cipher_seed, self.password_manager_api(), &self.store), self.password_manager_api(), ) } /// Returns the Signer API for the wallet if the cipher seed has been initialized. This signer uses the local key /// store where key material is kept in the local database. - /// - /// ## Panics - /// This function will panic if the cipher seed has not been initialized i.e. `initialize_cipher_seed` has not been - /// called once before calling this. pub fn local_signer_api(&self) -> LocalSignerApi<'_, TStore> { - let cipher_seed = self - .loaded_cipher_seed - .cipher_seed() - .expect("Cipher seed not initialized"); - let backend = LocalKeyManager::new( - cipher_seed, - LocalKeyStore::new(self.password_manager_api(), &self.store), - ); + let store = LocalKeyStore::new(&self.loaded_cipher_seed, self.password_manager_api(), &self.store); + let backend = LocalKeyManager::new(store); SignerApi::new(backend) } diff --git a/utilities/tariswap_test_bench/src/tariswap.rs b/utilities/tariswap_test_bench/src/tariswap.rs index 25eefdd152..e79daa84c6 100644 --- a/utilities/tariswap_test_bench/src/tariswap.rs +++ b/utilities/tariswap_test_bench/src/tariswap.rs @@ -146,7 +146,7 @@ impl Runner { .put_last_instruction_output_on_workspace("lp") .call_method(account.component_address, "deposit", args![Workspace("lp")]) .with_authorized_seal_signer() - .then(|builder| { + .map(|builder| { // First sign with the account key to authorize the use of the account component self.sdk.local_signer_api().sign_with_context( KeyBranch::Account, @@ -350,6 +350,7 @@ impl Runner { .call_method(tariswap.component_address, "swap", args![Workspace("b"), XTR,]) .put_last_instruction_output_on_workspace("swapped") .call_method(account.component_address, "deposit", args![Workspace("swapped")]) + .with_authorized_seal_signer() .build(); let transaction = self.sdk.local_signer_api().sign( From 788b18a8c769524c14814f7340876bc3cc092b48 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Wed, 15 Oct 2025 16:05:59 +0400 Subject: [PATCH 2/4] fix!: require a specific signer for input spend --- .../src/transaction_executor.rs | 6 +- .../tari_walletd/src/handlers/accounts.rs | 31 ++-- .../tari_walletd/src/handlers/transaction.rs | 2 +- crates/engine/src/executables/mod.rs | 1 + crates/engine/src/executables/transaction.rs | 11 +- crates/engine/src/runtime/error.rs | 3 + crates/engine/src/runtime/working_state.rs | 16 ++- crates/engine/tests/signature.rs | 20 ++- crates/engine/tests/stealth.rs | 132 ++++++++++++++---- crates/engine/tests/test.rs | 2 +- crates/engine_types/src/crypto/messages.rs | 2 + crates/engine_types/src/stealth/transfer.rs | 9 +- crates/p2p/proto/transaction.proto | 1 + crates/p2p/src/conversions/transaction.rs | 3 + .../templates/faucet/src/lib.rs | 3 +- .../template_lib/src/models/non_fungible.rs | 4 +- crates/template_lib/src/models/stealth.rs | 18 ++- .../src/support/stealth.rs | 10 ++ .../src/template_test.rs | 33 +++-- .../src/wrapped_transaction.rs | 4 + crates/transaction/src/builder/mod.rs | 2 +- .../transaction/src/unsigned_transaction.rs | 6 +- crates/wallet/crypto/src/balance_proof.rs | 3 +- crates/wallet/crypto/src/stealth.rs | 5 + .../tests/stealth_transfer_statement.rs | 39 +++++- crates/wallet/sdk/src/apis/stealth_crypto.rs | 2 + crates/wallet/sdk/src/apis/stealth_outputs.rs | 4 +- .../wallet/sdk/src/apis/stealth_transfer.rs | 110 +++++++++++---- crates/wallet/sdk/src/sdk.rs | 1 + 29 files changed, 357 insertions(+), 126 deletions(-) diff --git a/applications/tari_app_utilities/src/transaction_executor.rs b/applications/tari_app_utilities/src/transaction_executor.rs index a3f52ef61d..33171e674b 100644 --- a/applications/tari_app_utilities/src/transaction_executor.rs +++ b/applications/tari_app_utilities/src/transaction_executor.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use log::*; use tari_engine::{ + executables::Executable, fees::{FeeModule, FeeTable}, runtime::{AuthParams, RuntimeModule}, state_store::{memory::ReadOnlyMemoryStateStore, StateStoreError}, @@ -120,10 +121,7 @@ where TTemplateProvider: TemplateProvider