diff --git a/Cargo.lock b/Cargo.lock index ddd784064..5933b6e9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3213,6 +3213,15 @@ version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" +[[package]] +name = "generate_ristretto_value_lookup" +version = "0.4.1" +dependencies = [ + "clap 3.2.25", + "human_bytes", + "tari_crypto", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -3667,6 +3676,12 @@ dependencies = [ "url", ] +[[package]] +name = "human_bytes" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e" + [[package]] name = "humantime" version = "1.3.0" @@ -9234,6 +9249,24 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "tari_dan_wallet_crypto" +version = "0.4.1" +dependencies = [ + "blake2", + "chacha20poly1305", + "digest 0.10.7", + "rand", + "tari_crypto", + "tari_engine_types", + "tari_hash_domains", + "tari_template_lib", + "tari_template_test_tooling", + "tari_utilities", + "thiserror", + "zeroize", +] + [[package]] name = "tari_dan_wallet_daemon" version = "0.4.1" @@ -9262,6 +9295,7 @@ dependencies = [ "tari_crypto", "tari_dan_app_utilities", "tari_dan_common_types", + "tari_dan_wallet_crypto", "tari_dan_wallet_sdk", "tari_dan_wallet_storage_sqlite", "tari_engine_types", @@ -9288,13 +9322,10 @@ dependencies = [ "anyhow", "async-trait", "blake2", - "chacha20poly1305", "chrono", "digest 0.10.7", "jsonwebtoken", - "lazy_static", "log", - "rand", "serde", "serde_json", "tari_bor", @@ -9302,9 +9333,9 @@ dependencies = [ "tari_crypto", "tari_dan_common_types", "tari_dan_storage", + "tari_dan_wallet_crypto", "tari_dan_wallet_storage_sqlite", "tari_engine_types", - "tari_hash_domains", "tari_key_manager", "tari_template_abi", "tari_template_lib", @@ -9313,7 +9344,6 @@ dependencies = [ "tempfile", "thiserror", "ts-rs", - "zeroize", ] [[package]] @@ -9878,6 +9908,7 @@ dependencies = [ "tari_crypto", "tari_dan_common_types", "tari_dan_engine", + "tari_dan_wallet_crypto", "tari_engine_types", "tari_template_builtin", "tari_template_lib", @@ -10179,18 +10210,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 12b12d07c..8ebee152e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ members = [ "dan_layer/transaction_manifest", "dan_layer/transaction", "dan_layer/validator_node_rpc", + "dan_layer/wallet/crypto", "dan_layer/wallet/sdk", "dan_layer/wallet/storage_sqlite", "integration_tests", @@ -53,6 +54,7 @@ members = [ "utilities/tariswap_test_bench", "utilities/transaction_submitter", "utilities/transaction_submitter", + "utilities/generate_ristretto_value_lookup", ] resolver = "2" @@ -78,6 +80,7 @@ tari_dan_storage = { path = "dan_layer/storage" } tari_dan_storage_sqlite = { path = "dan_layer/storage_sqlite" } tari_dan_wallet_daemon = { path = "applications/tari_dan_wallet_daemon" } tari_dan_wallet_sdk = { path = "dan_layer/wallet/sdk" } +tari_dan_wallet_crypto = { path = "dan_layer/wallet/crypto" } tari_dan_wallet_storage_sqlite = { path = "dan_layer/wallet/storage_sqlite" } tari_dan_p2p = { path = "dan_layer/p2p" } tari_engine_types = { path = "dan_layer/engine_types" } diff --git a/applications/tari_dan_wallet_daemon/Cargo.toml b/applications/tari_dan_wallet_daemon/Cargo.toml index 5f7aa1d7f..26bba7d33 100644 --- a/applications/tari_dan_wallet_daemon/Cargo.toml +++ b/applications/tari_dan_wallet_daemon/Cargo.toml @@ -15,6 +15,7 @@ tari_crypto = { workspace = true } tari_common_types = { workspace = true } tari_dan_app_utilities = { workspace = true } tari_shutdown = { workspace = true } +tari_dan_wallet_crypto = { workspace = true } tari_dan_wallet_sdk = { workspace = true } tari_dan_wallet_storage_sqlite = { workspace = true } tari_transaction = { workspace = true } diff --git a/applications/tari_dan_wallet_daemon/src/handlers/accounts.rs b/applications/tari_dan_wallet_daemon/src/handlers/accounts.rs index 392752c40..b48dbe8a0 100644 --- a/applications/tari_dan_wallet_daemon/src/handlers/accounts.rs +++ b/applications/tari_dan_wallet_daemon/src/handlers/accounts.rs @@ -14,9 +14,9 @@ use tari_crypto::{ tari_utilities::ByteArray, }; use tari_dan_common_types::optional::Optional; +use tari_dan_wallet_crypto::ConfidentialProofStatement; use tari_dan_wallet_sdk::{ apis::{jwt::JrpcPermission, key_manager, substate::ValidatorScanResult}, - confidential::{get_commitment_factory, ConfidentialProofStatement}, models::{ConfidentialOutputModel, OutputStatus, VersionedSubstateId}, storage::WalletStore, DanWalletSdk, @@ -24,7 +24,7 @@ use tari_dan_wallet_sdk::{ use tari_dan_wallet_storage_sqlite::SqliteWalletStore; use tari_engine_types::{ component::new_account_address_from_parts, - confidential::ConfidentialClaim, + confidential::{get_commitment_factory, ConfidentialClaim}, instruction::Instruction, substate::{Substate, SubstateId}, }; @@ -387,6 +387,7 @@ pub async fn handle_reveal_funds( minimum_value_promise: 0, encrypted_data, reveal_amount: amount_to_reveal, + resource_view_key: None, }; let inputs = sdk @@ -623,6 +624,7 @@ pub async fn handle_claim_burn( minimum_value_promise: 0, encrypted_data, reveal_amount: max_fee, + resource_view_key: None, }; let reveal_proof = sdk.confidential_crypto_api().generate_withdraw_proof( @@ -1104,6 +1106,12 @@ pub async fn handle_confidential_transfer( .scan_for_substate(&SubstateId::Resource(req.resource_address), None) .await?; inputs.push(resource_substate.address); + let resource_view_key = resource_substate + .substate + .as_resource() + .ok_or_else(|| anyhow!("Indexer returned a non-resource substate when requesting a resource address"))? + .view_key() + .cloned(); // get destination account information let destination_account_address = @@ -1132,6 +1140,7 @@ pub async fn handle_confidential_transfer( encrypted_data, minimum_value_promise: 0, reveal_amount: Amount::zero(), + resource_view_key: resource_view_key.clone(), }; let change_amount = total_input_value - req.amount.as_u64_checked().unwrap(); @@ -1168,6 +1177,7 @@ pub async fn handle_confidential_transfer( minimum_value_promise: 0, encrypted_data, reveal_amount: Amount::zero(), + resource_view_key, }) } else { None diff --git a/applications/tari_dan_wallet_daemon/src/handlers/confidential.rs b/applications/tari_dan_wallet_daemon/src/handlers/confidential.rs index e2dee4533..265cbe016 100644 --- a/applications/tari_dan_wallet_daemon/src/handlers/confidential.rs +++ b/applications/tari_dan_wallet_daemon/src/handlers/confidential.rs @@ -9,11 +9,13 @@ use rand::rngs::OsRng; use serde_json::json; use tari_common_types::types::PublicKey; use tari_crypto::{commitment::HomomorphicCommitmentFactory, keys::PublicKey as _}; +use tari_dan_common_types::optional::Optional; +use tari_dan_wallet_crypto::ConfidentialProofStatement; use tari_dan_wallet_sdk::{ apis::{jwt::JrpcPermission, key_manager}, - confidential::{get_commitment_factory, ConfidentialProofStatement}, models::{ConfidentialOutputModel, OutputStatus}, }; +use tari_engine_types::confidential::get_commitment_factory; use tari_template_lib::models::Amount; use tari_wallet_daemon_client::types::{ ConfidentialCreateOutputProofRequest, @@ -31,6 +33,7 @@ use crate::handlers::{ const LOG_TARGET: &str = "tari::dan::wallet_daemon::json_rpc::confidential"; +#[allow(clippy::too_many_lines)] pub async fn handle_create_transfer_proof( context: &HandlerContext, token: Option, @@ -88,6 +91,26 @@ pub async fn handle_create_transfer_proof( &account_secret.key, )?; + let known_resource_substate_address = sdk + .substate_api() + .get_substate(&req.resource_address.into()) + .optional()?; + let resource = sdk + .substate_api() + .scan_for_substate( + &req.resource_address.into(), + known_resource_substate_address.map(|s| s.address.version), + ) + .await?; + let resource_view_key = resource + .substate + .as_resource() + .ok_or_else(|| { + anyhow::anyhow!("Indexer returned a non-resource substate when scanning for a resource address") + })? + .view_key() + .cloned(); + let output_statement = ConfidentialProofStatement { amount: req.amount, mask: output_mask.key, @@ -95,6 +118,7 @@ pub async fn handle_create_transfer_proof( minimum_value_promise: 0, encrypted_data, reveal_amount: req.reveal_amount, + resource_view_key: resource_view_key.clone(), }; let change_amount = total_input_value - req.amount.value() as u64 - req.reveal_amount.value() as u64; @@ -129,6 +153,7 @@ pub async fn handle_create_transfer_proof( encrypted_data, minimum_value_promise: 0, reveal_amount: Amount::zero(), + resource_view_key, }) } else { None @@ -200,6 +225,8 @@ pub async fn handle_create_output_proof( minimum_value_promise: 0, encrypted_data, reveal_amount: Amount::zero(), + // TODO: the request must include the resource address so that we can fetch the view key + resource_view_key: None, }; let proof = sdk.confidential_crypto_api().generate_output_proof(&statement)?; Ok(ConfidentialCreateOutputProofResponse { proof }) diff --git a/applications/tari_indexer/src/json_rpc/json_encoding.rs b/applications/tari_indexer/src/json_rpc/json_encoding.rs index 291c1db68..01d0e4924 100644 --- a/applications/tari_indexer/src/json_rpc/json_encoding.rs +++ b/applications/tari_indexer/src/json_rpc/json_encoding.rs @@ -176,6 +176,7 @@ mod tests { stealth_public_nonce: commitment.as_public_key().clone(), encrypted_data: Default::default(), minimum_value_promise: 0, + viewable_balance: None, }; let commitment = Some((commitment, confidential_output)); diff --git a/applications/tari_validator_node/src/bootstrap.rs b/applications/tari_validator_node/src/bootstrap.rs index 51398e454..313a2431d 100644 --- a/applications/tari_validator_node/src/bootstrap.rs +++ b/applications/tari_validator_node/src/bootstrap.rs @@ -476,6 +476,7 @@ where OwnerRule::None, ResourceAccessRules::new(), metadata, + None, ) .into(), state_hash: Default::default(), @@ -503,6 +504,7 @@ where OwnerRule::None, ResourceAccessRules::new(), metadata, + None, ) .into(), state_hash: Default::default(), diff --git a/dan_layer/engine/src/bootstrap.rs b/dan_layer/engine/src/bootstrap.rs index 8ffd53e75..1bad48d9b 100644 --- a/dan_layer/engine/src/bootstrap.rs +++ b/dan_layer/engine/src/bootstrap.rs @@ -30,6 +30,7 @@ pub fn bootstrap_state(state_db: &mut T) -> Result<(), StateStor OwnerRule::None, ResourceAccessRules::deny_all(), metadata, + None, ), ), )?; @@ -51,6 +52,7 @@ pub fn bootstrap_state(state_db: &mut T) -> Result<(), StateStor .withdrawable(AccessRule::AllowAll) .depositable(AccessRule::AllowAll), metadata, + None, ), ), )?; diff --git a/dan_layer/engine/src/runtime/impl.rs b/dan_layer/engine/src/runtime/impl.rs index 50e256569..4e047076c 100644 --- a/dan_layer/engine/src/runtime/impl.rs +++ b/dan_layer/engine/src/runtime/impl.rs @@ -528,7 +528,26 @@ impl> RuntimeInte match action { ResourceAction::Create => { - let arg: CreateResourceArg = args.get(0)?; + let arg: CreateResourceArg = args.assert_one_arg()?; + + if arg + .mint_arg + .as_ref() + .map(|mint| mint.as_resource_type() != arg.resource_type) + .unwrap_or(false) + { + return Err(RuntimeError::InvalidArgument { + argument: "CreateResourceArg", + reason: "Mint argument type does not match resource type".to_string(), + }); + } + + if arg.view_key.is_some() && !arg.resource_type.is_confidential() { + return Err(RuntimeError::InvalidArgument { + argument: "CreateResourceArg", + reason: "View key can only be set for confidential resources".to_string(), + }); + } let owner_key = match &arg.owner_rule { OwnerRule::OwnedBySigner => { @@ -538,6 +557,15 @@ impl> RuntimeInte OwnerRule::None | OwnerRule::ByAccessRule(_) => None, }; + let maybe_view_key = arg + .view_key + .map(|k| RistrettoPublicKey::from_canonical_bytes(k.as_ref())) + .transpose() + .map_err(|e| RuntimeError::InvalidArgument { + argument: "CreateResourceArg", + reason: format!("Invalid view key: {}", e), + })?; + self.tracker.write_with(|state| { let resource = Resource::new( arg.resource_type, @@ -545,6 +573,7 @@ impl> RuntimeInte arg.owner_rule, arg.access_rules, arg.metadata, + maybe_view_key, ); let resource_address = state.id_provider()?.new_resource_address()?; @@ -614,10 +643,13 @@ impl> RuntimeInte self.tracker.write_with(|state| { let resource_lock = state.lock_substate(&SubstateId::Resource(resource_address), LockFlag::Write)?; + let resource = state.get_resource(&resource_lock)?; - state - .authorization() - .check_resource_access_rules(ResourceAuthAction::Mint, &resource_lock)?; + state.authorization().check_resource_access_rules( + ResourceAuthAction::Mint, + resource.as_ownership(), + resource.access_rules(), + )?; let resource = state.mint_resource(&resource_lock, mint_resource.mint_arg)?; let bucket_id = state.id_provider()?.new_bucket_id(); @@ -642,10 +674,13 @@ impl> RuntimeInte self.tracker.write_with(|state| { let resource_lock = state.lock_substate(&SubstateId::Resource(resource_address), LockFlag::Write)?; + let resource = state.get_resource(&resource_lock)?; - state - .authorization() - .check_resource_access_rules(ResourceAuthAction::Recall, &resource_lock)?; + state.authorization().check_resource_access_rules( + ResourceAuthAction::Recall, + resource.as_ownership(), + resource.access_rules(), + )?; let vault_lock = state.lock_substate(&arg.vault_id.into(), LockFlag::Write)?; @@ -703,10 +738,13 @@ impl> RuntimeInte self.tracker.write_with(|state| { let resource_lock = state.lock_substate(&SubstateId::Resource(resource_address), LockFlag::Read)?; + let resource = state.get_resource(&resource_lock)?; - state - .authorization() - .check_resource_access_rules(ResourceAuthAction::UpdateNonFungibleData, &resource_lock)?; + state.authorization().check_resource_access_rules( + ResourceAuthAction::UpdateNonFungibleData, + resource.as_ownership(), + resource.access_rules(), + )?; let addr = NonFungibleAddress::new(resource_address, arg.id); let locked = state.lock_substate(&SubstateId::NonFungible(addr.clone()), LockFlag::Write)?; @@ -787,11 +825,14 @@ impl> RuntimeInte self.tracker.write_with(|state| { let substate_id = SubstateId::Resource(*resource_address); let resource_lock = state.lock_substate(&substate_id, LockFlag::Read)?; + let resource = state.get_resource(&resource_lock)?; // Require deposit permissions on the resource to create the vault (even if empty) - state - .authorization() - .check_resource_access_rules(ResourceAuthAction::Deposit, &resource_lock)?; + state.authorization().check_resource_access_rules( + ResourceAuthAction::Deposit, + resource.as_ownership(), + resource.access_rules(), + )?; let resource_type = state.get_resource(&resource_lock)?.resource_type(); let vault_id = state.id_provider()?.new_vault_id()?; @@ -837,9 +878,13 @@ impl> RuntimeInte let resource_lock = state.lock_substate(&SubstateId::Resource(*resource_address), LockFlag::Read)?; - state - .authorization() - .check_resource_access_rules(ResourceAuthAction::Deposit, &resource_lock)?; + let resource = state.get_resource(&resource_lock)?; + + state.authorization().check_resource_access_rules( + ResourceAuthAction::Deposit, + resource.as_ownership(), + resource.access_rules(), + )?; // It is invalid to deposit a bucket that has locked funds let bucket = state.take_bucket(bucket_id)?; @@ -872,15 +917,24 @@ impl> RuntimeInte let resource_address = *vault.resource_address(); let resource_lock = state.lock_substate(&SubstateId::Resource(resource_address), LockFlag::Read)?; - state - .authorization() - .check_resource_access_rules(ResourceAuthAction::Withdraw, &resource_lock)?; + let resource = state.get_resource(&resource_lock)?; + + state.authorization().check_resource_access_rules( + ResourceAuthAction::Withdraw, + resource.as_ownership(), + resource.access_rules(), + )?; + + let resource = state.get_resource(&resource_lock)?; + let maybe_view_key = resource.view_key().cloned(); let vault_mut = state.get_vault_mut(&vault_lock)?; let resource_container = match arg { VaultWithdrawArg::Fungible { amount } => vault_mut.withdraw(amount)?, VaultWithdrawArg::NonFungible { ids } => vault_mut.withdraw_non_fungibles(&ids)?, - VaultWithdrawArg::Confidential { proof } => vault_mut.withdraw_confidential(*proof)?, + VaultWithdrawArg::Confidential { proof } => { + vault_mut.withdraw_confidential(*proof, maybe_view_key.as_ref())? + }, }; let bucket_id = state.id_provider()?.new_bucket_id(); @@ -964,12 +1018,17 @@ impl> RuntimeInte let resource_address = state.get_vault(&vault_lock)?.resource_address(); let resource_lock = state.lock_substate(&SubstateId::Resource(*resource_address), LockFlag::Read)?; - state - .authorization() - .check_resource_access_rules(ResourceAuthAction::Withdraw, &resource_lock)?; + let resource = state.get_resource(&resource_lock)?; + + state.authorization().check_resource_access_rules( + ResourceAuthAction::Withdraw, + resource.as_ownership(), + resource.access_rules(), + )?; + let view_key = resource.view_key().cloned(); let vault_mut = state.get_vault_mut(&vault_lock)?; - let resource_container = vault_mut.reveal_confidential(arg.proof)?; + let resource_container = vault_mut.reveal_confidential(arg.proof, view_key.as_ref())?; let bucket_id = state.id_provider()?.new_bucket_id(); state.new_bucket(bucket_id, resource_container)?; @@ -998,31 +1057,35 @@ impl> RuntimeInte let vault_lock = state.lock_substate(&SubstateId::Vault(vault_id), LockFlag::Write)?; let resource_address = *state.get_vault(&vault_lock)?.resource_address(); let resource_lock = state.lock_substate(&SubstateId::Resource(resource_address), LockFlag::Read)?; + let resource = state.get_resource(&resource_lock)?; - state - .authorization() - .check_resource_access_rules(ResourceAuthAction::Withdraw, &resource_lock)?; + state.authorization().check_resource_access_rules( + ResourceAuthAction::Withdraw, + resource.as_ownership(), + resource.access_rules(), + )?; + let view_key = resource.view_key().cloned(); let vault_mut = state.get_vault_mut(&vault_lock)?; - let mut resource = + let mut container = ResourceContainer::confidential(*vault_mut.resource_address(), None, Amount::zero()); if !arg.amount.is_zero() { let withdrawn = vault_mut.withdraw(arg.amount)?; - resource.deposit(withdrawn)?; + container.deposit(withdrawn)?; } if let Some(proof) = arg.proof { - let revealed = vault_mut.reveal_confidential(proof)?; - resource.deposit(revealed)?; + let revealed = vault_mut.reveal_confidential(proof, view_key.as_ref())?; + container.deposit(revealed)?; } - if resource.amount().is_zero() { + if container.amount().is_zero() { return Err(RuntimeError::InvalidArgument { argument: "TakeFeesArg", reason: "Fee payment has zero value".to_string(), }); } - state.pay_fee(resource, vault_id)?; + state.pay_fee(container, vault_id)?; state.unlock_substate(resource_lock)?; state.unlock_substate(vault_lock)?; @@ -1042,10 +1105,13 @@ impl> RuntimeInte let vault = state.get_vault(&vault_lock)?; let resource_address = *vault.resource_address(); let resource_lock = state.lock_substate(&SubstateId::Resource(resource_address), LockFlag::Read)?; + let resource = state.get_resource(&resource_lock)?; - state - .authorization() - .check_resource_access_rules(ResourceAuthAction::Withdraw, &resource_lock)?; + state.authorization().check_resource_access_rules( + ResourceAuthAction::Withdraw, + resource.as_ownership(), + resource.access_rules(), + )?; let proof_id = state.id_provider()?.new_proof_id(); let vault_mut = state.get_vault_mut(&vault_lock)?; @@ -1070,10 +1136,13 @@ impl> RuntimeInte let vault = state.get_vault(&vault_lock)?; let resource_address = *vault.resource_address(); let resource_lock = state.lock_substate(&SubstateId::Resource(resource_address), LockFlag::Read)?; + let resource = state.get_resource(&resource_lock)?; - state - .authorization() - .check_resource_access_rules(ResourceAuthAction::Withdraw, &resource_lock)?; + state.authorization().check_resource_access_rules( + ResourceAuthAction::Withdraw, + resource.as_ownership(), + resource.access_rules(), + )?; let proof_id = state.id_provider()?.new_proof_id(); let vault_mut = state.get_vault_mut(&vault_lock)?; @@ -1098,10 +1167,13 @@ impl> RuntimeInte let vault = state.get_vault(&vault_lock)?; let resource_address = *vault.resource_address(); let resource_lock = state.lock_substate(&SubstateId::Resource(resource_address), LockFlag::Read)?; + let resource = state.get_resource(&resource_lock)?; - state - .authorization() - .check_resource_access_rules(ResourceAuthAction::Withdraw, &resource_lock)?; + state.authorization().check_resource_access_rules( + ResourceAuthAction::Withdraw, + resource.as_ownership(), + resource.access_rules(), + )?; let proof_id = state.id_provider()?.new_proof_id(); let vault_mut = state.get_vault_mut(&vault_lock)?; @@ -1211,10 +1283,15 @@ impl> RuntimeInte let proof = args.assert_one_arg()?; self.tracker.write_with(|state| { - let bucket = state.get_bucket_mut(bucket_id)?; - let resource = bucket.take_confidential(proof)?; + let bucket = state.get_bucket(bucket_id)?; + let resource_lock = state.lock_substate(&(*bucket.resource_address()).into(), LockFlag::Read)?; + let resource = state.get_resource(&resource_lock)?; + let view_key = resource.view_key().cloned(); + let bucket_mut = state.get_bucket_mut(bucket_id)?; + let resource = bucket_mut.take_confidential(proof, view_key.as_ref())?; let bucket_id = state.id_provider()?.new_bucket_id(); state.new_bucket(bucket_id, resource)?; + state.unlock_substate(resource_lock)?; Ok(InvokeResult::encode(&bucket_id)?) }) }, @@ -1225,10 +1302,15 @@ impl> RuntimeInte })?; let proof = args.assert_one_arg()?; self.tracker.write_with(|state| { + let bucket = state.get_bucket(bucket_id)?; + let resource_lock = state.lock_substate(&(*bucket.resource_address()).into(), LockFlag::Read)?; + let resource = state.get_resource(&resource_lock)?; + let view_key = resource.view_key().cloned(); let bucket = state.get_bucket_mut(bucket_id)?; - let resource = bucket.reveal_confidential(proof)?; + let resource = bucket.reveal_confidential(proof, view_key.as_ref())?; let bucket_id = state.id_provider()?.new_bucket_id(); state.new_bucket(bucket_id, resource)?; + state.unlock_substate(resource_lock)?; Ok(InvokeResult::encode(&bucket_id)?) }) }, @@ -1244,15 +1326,19 @@ impl> RuntimeInte let resource_lock = state.lock_substate(&SubstateId::Resource(resource_address), LockFlag::Write)?; - state - .authorization() - .check_resource_access_rules(ResourceAuthAction::Burn, &resource_lock)?; + let resource = state.get_resource(&resource_lock)?; + + state.authorization().check_resource_access_rules( + ResourceAuthAction::Burn, + resource.as_ownership(), + resource.access_rules(), + )?; let burnt_amount = bucket.amount(); state.burn_bucket(bucket)?; - let resource = state.get_resource_mut(&resource_lock)?; - resource.decrease_total_supply(burnt_amount); + let resource_mut = state.get_resource_mut(&resource_lock)?; + resource_mut.decrease_total_supply(burnt_amount); state.unlock_substate(resource_lock)?; @@ -1270,11 +1356,14 @@ impl> RuntimeInte self.tracker.write_with(|state| { let locked_funds = state.get_bucket_mut(bucket_id)?.lock_all()?; let resource_address = *locked_funds.resource_address(); - let resource_lock = state.lock_substate(&SubstateId::Resource(resource_address), LockFlag::Read)?; - state - .authorization() - .check_resource_access_rules(ResourceAuthAction::Withdraw, &resource_lock)?; + let resource = state.get_resource(&resource_lock)?; + + state.authorization().check_resource_access_rules( + ResourceAuthAction::Withdraw, + resource.as_ownership(), + resource.access_rules(), + )?; let proof_id = state.id_provider()?.new_proof_id(); state.new_proof(proof_id, locked_funds)?; @@ -1675,6 +1764,7 @@ impl> RuntimeInte stealth_public_nonce: diffie_hellman_public_key, encrypted_data: unclaimed_output.encrypted_data, minimum_value_promise: 0, + viewable_balance: None, })), Amount::zero(), ); @@ -1682,7 +1772,7 @@ impl> RuntimeInte // If a withdraw proof is provided, we execute it and deposit back into the resource // This allows some funds to be revealed and/or reblinded within a single instruction if let Some(proof) = withdraw_proof { - let withdraw = resource.withdraw_confidential(proof)?; + let withdraw = resource.withdraw_confidential(proof, None)?; resource.deposit(withdraw)?; } diff --git a/dan_layer/engine/src/runtime/tracker_auth.rs b/dan_layer/engine/src/runtime/tracker_auth.rs index 45bf58563..3820875a4 100644 --- a/dan_layer/engine/src/runtime/tracker_auth.rs +++ b/dan_layer/engine/src/runtime/tracker_auth.rs @@ -6,6 +6,7 @@ use tari_template_lib::auth::{ OwnerRule, Ownership, RequireRule, + ResourceAccessRules, ResourceAuthAction, RestrictedAccessRule, RuleRequirement, @@ -61,19 +62,19 @@ impl<'a> Authorization<'a> { pub fn check_resource_access_rules( &self, action: ResourceAuthAction, - locked: &LockedSubstate, + resource_ownership: Ownership<'_>, + resource_access_rules: &ResourceAccessRules, ) -> Result<(), RuntimeError> { - let resource = self.state.get_resource(locked)?; let scope = self.state.current_call_scope()?.auth_scope(); // Check ownership. // A resource is only recallable by explicit access rules - if !action.is_recall() && check_ownership(self.state, scope, resource.as_ownership())? { + if !action.is_recall() && check_ownership(self.state, scope, resource_ownership)? { // Owner can invoke any resource method return Ok(()); } - let rule = resource.access_rules().get_access_rule(&action); + let rule = resource_access_rules.get_access_rule(&action); if !check_access_rule(self.state, scope, rule)? { return Err(RuntimeError::AccessDenied { action_ident: action.into(), diff --git a/dan_layer/engine/src/runtime/working_state.rs b/dan_layer/engine/src/runtime/working_state.rs index 397635787..9bd576ba4 100644 --- a/dan_layer/engine/src/runtime/working_state.rs +++ b/dan_layer/engine/src/runtime/working_state.rs @@ -531,11 +531,12 @@ impl WorkingState { ResourceContainer::non_fungible(resource_address, token_ids) }, MintArg::Confidential { proof } => { + let resource = self.get_resource(locked_resource)?; debug!( target: LOG_TARGET, "Minting confidential tokens on resource: {}", resource_address ); - ResourceContainer::mint_confidential(resource_address, *proof)? + ResourceContainer::mint_confidential(resource_address, *proof, resource.view_key())? }, }; diff --git a/dan_layer/engine/tests/confidential.rs b/dan_layer/engine/tests/confidential.rs index 5e989794c..fb00cb189 100644 --- a/dan_layer/engine/tests/confidential.rs +++ b/dan_layer/engine/tests/confidential.rs @@ -1,31 +1,55 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause +use rand::rngs::OsRng; +use tari_common_types::types::PublicKey; +use tari_crypto::keys::PublicKey as _; use tari_engine_types::{resource_container::ResourceError, substate::SubstateId}; use tari_template_lib::{ args, + crypto::RistrettoPublicKeyBytes, models::{Amount, ComponentAddress}, prelude::ConfidentialOutputProof, }; use tari_template_test_tooling::{ support::{ assert_error::assert_reject_reason, - confidential::{generate_confidential_proof, generate_withdraw_proof, generate_withdraw_proof_with_inputs}, + confidential::{ + generate_confidential_proof, + generate_confidential_proof_with_view_key, + generate_withdraw_proof, + generate_withdraw_proof_with_inputs, + generate_withdraw_proof_with_view_key, + }, + value_lookup_tables::AlwaysMissLookupTable, }, SubstateType, TemplateTest, }; use tari_transaction::Transaction; use tari_transaction_manifest::ManifestValue; +use tari_utilities::ByteArray; -fn setup(initial_supply: ConfidentialOutputProof) -> (TemplateTest, ComponentAddress, SubstateId) { +fn setup( + initial_supply: ConfidentialOutputProof, + view_key: Option<&PublicKey>, +) -> (TemplateTest, ComponentAddress, SubstateId) { let mut template_test = TemplateTest::new(vec![ "tests/templates/confidential/faucet", "tests/templates/confidential/utilities", ]); - let faucet: ComponentAddress = - template_test.call_function("ConfidentialFaucet", "mint", args![initial_supply], vec![]); + let faucet: ComponentAddress = view_key + .map(|vk| { + let vk = RistrettoPublicKeyBytes::from_bytes(vk.as_bytes()).unwrap(); + template_test.call_function( + "ConfidentialFaucet", + "mint_with_view_key", + args![initial_supply, vk], + vec![], + ) + }) + .unwrap_or_else(|| template_test.call_function("ConfidentialFaucet", "mint", args![initial_supply], vec![])); let resx = template_test.get_previous_output_address(SubstateType::Resource); @@ -35,7 +59,7 @@ fn setup(initial_supply: ConfidentialOutputProof) -> (TemplateTest, ComponentAdd #[test] fn mint_initial_commitment() { let (confidential_proof, _mask, _change) = generate_confidential_proof(Amount(100), None); - let (mut template_test, faucet, _faucet_resx) = setup(confidential_proof); + let (mut template_test, faucet, _faucet_resx) = setup(confidential_proof, None); let total_supply: Amount = template_test.call_method(faucet, "total_supply", args![], vec![]); // The number of commitments @@ -47,7 +71,7 @@ fn mint_initial_commitment() { #[test] fn mint_more_later() { let (confidential_proof, _mask, _change) = generate_confidential_proof(Amount(0), None); - let (mut template_test, faucet, _faucet_resx) = setup(confidential_proof); + let (mut template_test, faucet, _faucet_resx) = setup(confidential_proof, None); let (confidential_proof, mask, _change) = generate_confidential_proof(Amount(100), None); template_test.call_method::<()>(faucet, "mint_more", args![confidential_proof], vec![]); @@ -70,7 +94,7 @@ fn mint_more_later() { #[test] fn transfer_confidential_amounts_between_accounts() { let (confidential_proof, faucet_mask, _change) = generate_confidential_proof(Amount(100_000), None); - let (mut template_test, faucet, faucet_resx) = setup(confidential_proof); + let (mut template_test, faucet, faucet_resx) = setup(confidential_proof, None); // Create an account let (account1, owner1, _k) = template_test.create_owned_account(); @@ -152,7 +176,7 @@ fn transfer_confidential_amounts_between_accounts() { #[test] fn transfer_confidential_fails_with_invalid_balance() { let (confidential_proof, faucet_mask, _change) = generate_confidential_proof(Amount(100_000), None); - let (mut template_test, faucet, _faucet_resx) = setup(confidential_proof); + let (mut template_test, faucet, _faucet_resx) = setup(confidential_proof, None); // Create an account let (account1, _owner1, _k) = template_test.create_owned_account(); @@ -184,7 +208,7 @@ fn transfer_confidential_fails_with_invalid_balance() { #[test] fn reveal_confidential_and_transfer() { let (confidential_proof, faucet_mask, _change) = generate_confidential_proof(Amount(100_000), None); - let (mut template_test, faucet, faucet_resx) = setup(confidential_proof); + let (mut template_test, faucet, faucet_resx) = setup(confidential_proof, None); // Create an account let (account1, owner1, _k) = template_test.create_owned_account(); @@ -257,7 +281,7 @@ fn reveal_confidential_and_transfer() { #[test] fn attempt_to_reveal_with_unbalanced_proof() { let (confidential_proof, faucet_mask, _change) = generate_confidential_proof(Amount(100_000), None); - let (mut template_test, faucet, faucet_resx) = setup(confidential_proof); + let (mut template_test, faucet, faucet_resx) = setup(confidential_proof, None); // Create an account let (account1, owner1, _k) = template_test.create_owned_account(); @@ -310,7 +334,7 @@ fn attempt_to_reveal_with_unbalanced_proof() { #[test] fn multi_commitment_join() { let (confidential_proof, faucet_mask, _change) = generate_confidential_proof(Amount(100_000), None); - let (mut template_test, faucet, faucet_resx) = setup(confidential_proof); + let (mut template_test, faucet, faucet_resx) = setup(confidential_proof, None); // Create an account let (account1, owner1, _k) = template_test.create_owned_account(); @@ -390,7 +414,7 @@ fn multi_commitment_join() { #[test] fn mint_and_transfer_revealed() { let (confidential_proof, _mask, _change) = generate_confidential_proof(Amount(100), None); - let (mut test, faucet, faucet_resx) = setup(confidential_proof); + let (mut test, faucet, faucet_resx) = setup(confidential_proof, None); let faucet_resx = faucet_resx.as_resource_address().unwrap(); @@ -422,7 +446,7 @@ fn mint_and_transfer_revealed() { #[test] fn mint_revealed_with_invalid_proof() { let (confidential_proof, _mask, _change) = generate_confidential_proof(Amount(100), None); - let (mut test, faucet, _faucet_resx) = setup(confidential_proof); + let (mut test, faucet, _faucet_resx) = setup(confidential_proof, None); let reason = test.execute_expect_failure( Transaction::builder() @@ -436,3 +460,58 @@ fn mint_revealed_with_invalid_proof() { details: String::new(), }); } + +#[test] +fn mint_with_view_key() { + let (view_key_secret, ref view_key) = PublicKey::random_keypair(&mut OsRng); + let (confidential_proof, _mask, _change) = generate_confidential_proof_with_view_key(Amount(123), None, view_key); + let (mut test, faucet, _faucet_resx) = setup(confidential_proof, Some(view_key)); + let faucet_entity_id = faucet.entity_id(); + + let (confidential_proof, mask, _change) = generate_confidential_proof_with_view_key(Amount(100), None, view_key); + test.call_method::<()>(faucet, "mint_more", args![confidential_proof], vec![]); + + let (user_account, user_proof, user_key) = test.create_empty_account(); + let user_account_entity_id = user_account.entity_id(); + + let withdraw_proof = generate_withdraw_proof_with_view_key( + &mask, + Amount(100), + Amount(55), + Some(Amount(100 - 55)), + Amount(0), + view_key, + ); + let result = test.execute_expect_success( + Transaction::builder() + .call_method(faucet, "take_free_coins", args![withdraw_proof.proof]) + .put_last_instruction_output_on_workspace("coins") + .call_method(user_account, "deposit", args![Workspace("coins")]) + .sign(&user_key) + .build(), + vec![user_proof], + ); + + let diff = result.finalize.result.accept().unwrap(); + let faucet_vault = diff + .up_iter() + .find(|(addr, _)| addr.is_vault() && addr.as_vault_id().unwrap().entity_id() == faucet_entity_id) + .map(|(_, vault)| vault.substate_value().as_vault().unwrap()) + .unwrap(); + + let total_balance = faucet_vault + .try_brute_force_confidential_balance(&view_key_secret, 0..=200, AlwaysMissLookupTable) + .unwrap(); + assert_eq!(total_balance, Some(223 - 55)); + + let user_vault = diff + .up_iter() + .find(|(addr, _)| addr.is_vault() && addr.as_vault_id().unwrap().entity_id() == user_account_entity_id) + .map(|(_, vault)| vault.substate_value().as_vault().unwrap()) + .unwrap(); + + let total_balance = user_vault + .try_brute_force_confidential_balance(&view_key_secret, 0..=200, AlwaysMissLookupTable) + .unwrap(); + assert_eq!(total_balance, Some(55)); +} diff --git a/dan_layer/engine/tests/templates/confidential/faucet/src/lib.rs b/dan_layer/engine/tests/templates/confidential/faucet/src/lib.rs index 29b8e2dc9..9bcbb419c 100644 --- a/dan_layer/engine/tests/templates/confidential/faucet/src/lib.rs +++ b/dan_layer/engine/tests/templates/confidential/faucet/src/lib.rs @@ -44,6 +44,23 @@ mod faucet_template { .create() } + pub fn mint_with_view_key( + confidential_proof: ConfidentialOutputProof, + view_key: RistrettoPublicKeyBytes, + ) -> Component { + let coins = ResourceBuilder::confidential() + .mintable(AccessRule::AllowAll) + .initial_supply(confidential_proof) + .with_view_key(view_key) + .build_bucket(); + + Component::new(Self { + vault: Vault::from_bucket(coins), + }) + .with_access_rules(AccessRules::allow_all()) + .create() + } + pub fn mint_revealed(&mut self, amount: Amount) { let proof = ConfidentialOutputProof::mint_revealed(amount); let bucket = ResourceManager::get(self.vault.resource_address()).mint_confidential(proof); diff --git a/dan_layer/engine_types/src/bucket.rs b/dan_layer/engine_types/src/bucket.rs index 5c644f3ff..daf1f2d61 100644 --- a/dan_layer/engine_types/src/bucket.rs +++ b/dan_layer/engine_types/src/bucket.rs @@ -23,6 +23,7 @@ use std::collections::BTreeSet; use serde::{Deserialize, Serialize}; +use tari_common_types::types::PublicKey; use tari_template_lib::{ models::{Amount, BucketId, ConfidentialWithdrawProof, NonFungibleId, ResourceAddress}, prelude::ResourceType, @@ -83,15 +84,20 @@ impl Bucket { self.resource_container.withdraw(amount) } - pub fn take_confidential(&mut self, proof: ConfidentialWithdrawProof) -> Result { - self.resource_container.withdraw_confidential(proof) + pub fn take_confidential( + &mut self, + proof: ConfidentialWithdrawProof, + view_key: Option<&PublicKey>, + ) -> Result { + self.resource_container.withdraw_confidential(proof, view_key) } pub fn reveal_confidential( &mut self, proof: ConfidentialWithdrawProof, + view_key: Option<&PublicKey>, ) -> Result { - self.resource_container.reveal_confidential(proof) + self.resource_container.reveal_confidential(proof, view_key) } pub fn lock_all(&mut self) -> Result { diff --git a/dan_layer/engine_types/src/confidential/elgamal.rs b/dan_layer/engine_types/src/confidential/elgamal.rs new file mode 100644 index 000000000..b499eddff --- /dev/null +++ b/dan_layer/engine_types/src/confidential/elgamal.rs @@ -0,0 +1,137 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use tari_bor::{Deserialize, Serialize}; +use tari_common_types::types::{PrivateKey, PublicKey}; +use tari_crypto::keys::PublicKey as _; +use tari_utilities::ByteArray; + +use crate::confidential::value_lookup_table::ValueLookupTable; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr( + feature = "ts", + derive(ts_rs::TS), + ts(export, export_to = "../../bindings/src/types/") +)] +pub struct ElgamalVerifiableBalance { + #[cfg_attr(feature = "ts", ts(type = "string"))] + pub encrypted: PublicKey, + #[cfg_attr(feature = "ts", ts(type = "string"))] + pub public_nonce: PublicKey, +} + +impl ElgamalVerifiableBalance { + pub fn brute_force_balance, TLookup: ValueLookupTable>( + &self, + view_private_key: &PrivateKey, + value_range: I, + mut lookup_table: TLookup, + ) -> Result, TLookup::Error> { + // V = E - pR + let balance = &self.encrypted - view_private_key * &self.public_nonce; + let balance_bytes = copy_fixed(balance.as_bytes()); + + for v in value_range { + let value = lookup_table.lookup(v)?.unwrap_or_else(|| { + let pk = PublicKey::from_secret_key(&PrivateKey::from(v)); + copy_fixed(pk.as_bytes()) + }); + if value == balance_bytes { + return Ok(Some(v)); + } + } + + Ok(None) + } +} + +fn copy_fixed(src: &[u8]) -> [u8; 32] { + let mut buf = [0u8; 32]; + buf.copy_from_slice(src); + buf +} + +#[cfg(test)] +mod tests { + use std::convert::Infallible; + + use rand::rngs::OsRng; + use tari_crypto::keys::SecretKey; + + use super::*; + + #[derive(Default)] + pub struct TestLookupTable; + + impl ValueLookupTable for TestLookupTable { + type Error = Infallible; + + fn lookup(&mut self, value: u64) -> Result, Self::Error> { + // This would be a sequential lookup in a real implementation + Ok(Some(copy_fixed( + PublicKey::from_secret_key(&PrivateKey::from(value)).as_bytes(), + ))) + } + } + + mod brute_force_balance { + use super::*; + + #[test] + fn it_finds_the_value() { + const VALUE: u64 = 5242; + let view_sk = &PrivateKey::random(&mut OsRng); + let (nonce_sk, nonce_pk) = PublicKey::random_keypair(&mut OsRng); + + let rp = nonce_sk * view_sk; + + let subject = ElgamalVerifiableBalance { + encrypted: PublicKey::from_secret_key(&rp) + PublicKey::from_secret_key(&PrivateKey::from(VALUE)), + public_nonce: nonce_pk, + }; + + let balance = subject + .brute_force_balance(view_sk, 0..=10000, TestLookupTable) + .unwrap(); + assert_eq!(balance, Some(VALUE)); + } + + #[test] + fn it_returns_the_value_equal_to_max_value() { + let view_sk = &PrivateKey::random(&mut OsRng); + let (nonce_sk, nonce_pk) = PublicKey::random_keypair(&mut OsRng); + + let rp = nonce_sk * view_sk; + + let subject = ElgamalVerifiableBalance { + encrypted: PublicKey::from_secret_key(&rp) + PublicKey::from_secret_key(&PrivateKey::from(10)), + public_nonce: nonce_pk, + }; + + let balance = subject.brute_force_balance(view_sk, 0..=10, TestLookupTable).unwrap(); + assert_eq!(balance, Some(10)); + + let balance = subject.brute_force_balance(view_sk, 10..=12, TestLookupTable).unwrap(); + assert_eq!(balance, Some(10)); + } + + #[test] + fn it_returns_none_if_the_value_out_of_range() { + let subject = ElgamalVerifiableBalance { + encrypted: PublicKey::from_secret_key(&PrivateKey::from(101)), + public_nonce: Default::default(), + }; + + let balance = subject + .brute_force_balance(&PrivateKey::default(), 0..=100, TestLookupTable) + .unwrap(); + assert_eq!(balance, None); + + let balance = subject + .brute_force_balance(&PrivateKey::default(), 102..=103, TestLookupTable) + .unwrap(); + assert_eq!(balance, None); + } + } +} diff --git a/dan_layer/engine_types/src/confidential/mod.rs b/dan_layer/engine_types/src/confidential/mod.rs index 5f05f2419..2ca7b8175 100644 --- a/dan_layer/engine_types/src/confidential/mod.rs +++ b/dan_layer/engine_types/src/confidential/mod.rs @@ -2,13 +2,18 @@ // SPDX-License-Identifier: BSD-3-Clause mod claim; +mod elgamal; mod proof; mod unclaimed; mod validation; +mod value_lookup_table; mod withdraw; -pub use claim::ConfidentialClaim; -pub use proof::{challenges, get_commitment_factory, get_range_proof_service}; -pub use unclaimed::UnclaimedConfidentialOutput; -pub use validation::validate_confidential_proof; -pub use withdraw::{validate_confidential_withdraw, ConfidentialOutput, ValidatedConfidentialWithdrawProof}; +pub use claim::*; +pub use elgamal::*; +pub use proof::*; +pub use unclaimed::*; +pub use validation::*; +pub use value_lookup_table::*; +pub(crate) use withdraw::validate_confidential_withdraw; +pub use withdraw::{ConfidentialOutput, ValidatedConfidentialWithdrawProof}; diff --git a/dan_layer/engine_types/src/confidential/proof.rs b/dan_layer/engine_types/src/confidential/proof.rs index f643ca59c..f3aa8d328 100644 --- a/dan_layer/engine_types/src/confidential/proof.rs +++ b/dan_layer/engine_types/src/confidential/proof.rs @@ -32,7 +32,10 @@ pub fn get_commitment_factory() -> &'static CommitmentFactory { pub mod challenges { use tari_common_types::types::{Commitment, PublicKey}; - use tari_template_lib::{models::Amount, Hash}; + use tari_template_lib::{ + models::{Amount, ViewableBalanceProofChallengeFields}, + Hash, + }; use crate::hashing::{hasher32, hasher64, EngineHashDomainLabel}; @@ -62,6 +65,18 @@ pub mod challenges { .result() } + pub fn viewable_balance_proof_challenge64( + commitment: &Commitment, + view_key: &PublicKey, + challenge_fields: ViewableBalanceProofChallengeFields<'_>, + ) -> [u8; 64] { + hasher64(EngineHashDomainLabel::ViewKey) + .chain(commitment) + .chain(view_key) + .chain(&challenge_fields) + .result() + } + pub fn confidential_commitment_proof32( public_key: &PublicKey, public_nonce: &PublicKey, diff --git a/dan_layer/engine_types/src/confidential/validation.rs b/dan_layer/engine_types/src/confidential/validation.rs index 24f8e2642..628e970da 100644 --- a/dan_layer/engine_types/src/confidential/validation.rs +++ b/dan_layer/engine_types/src/confidential/validation.rs @@ -1,16 +1,21 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_common_types::types::{Commitment, PublicKey}; +use tari_common_types::types::{Commitment, PrivateKey, PublicKey}; use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, extended_range_proof::{ExtendedRangeProofService, Statement}, - ristretto::bulletproofs_plus::RistrettoAggregatedPublicStatement, + keys::{PublicKey as _, SecretKey}, + ristretto::{bulletproofs_plus::RistrettoAggregatedPublicStatement, RistrettoSecretKey}, tari_utilities::ByteArray, }; -use tari_template_lib::models::{Amount, ConfidentialOutputProof}; +use tari_template_lib::models::{Amount, ConfidentialOutputProof, ViewableBalanceProof}; -use super::get_range_proof_service; -use crate::{confidential::ConfidentialOutput, resource_container::ResourceError}; +use super::{challenges, get_commitment_factory, get_range_proof_service}; +use crate::{ + confidential::{elgamal::ElgamalVerifiableBalance, ConfidentialOutput}, + resource_container::ResourceError, +}; #[derive(Debug)] pub struct ValidatedConfidentialProof { @@ -22,6 +27,7 @@ pub struct ValidatedConfidentialProof { pub fn validate_confidential_proof( proof: &ConfidentialOutputProof, + view_key: Option<&PublicKey>, ) -> Result { if proof.output_revealed_amount.is_negative() || proof.change_revealed_amount.is_negative() { return Err(ResourceError::InvalidConfidentialProof { @@ -33,50 +39,62 @@ pub fn validate_confidential_proof( .output_statement .as_ref() .map(|statement| { - let output_commitment = Commitment::from_canonical_bytes(&statement.commitment).map_err(|_| { - ResourceError::InvalidConfidentialProof { - details: "Invalid commitment".to_string(), - } - })?; + let output_commitment = + Commitment::from_canonical_bytes(statement.commitment.as_bytes()).map_err(|_| { + ResourceError::InvalidConfidentialProof { + details: "Invalid commitment".to_string(), + } + })?; let output_public_nonce = PublicKey::from_canonical_bytes(statement.sender_public_nonce.as_bytes()) .map_err(|_| ResourceError::InvalidConfidentialProof { details: "Invalid sender public nonce".to_string(), })?; + let viewable_balance = validate_elgamal_verifiable_balance_proof( + &output_commitment, + view_key, + statement.viewable_balance_proof.as_ref(), + )?; + Ok(ConfidentialOutput { commitment: output_commitment, stealth_public_nonce: output_public_nonce, encrypted_data: statement.encrypted_data.clone(), minimum_value_promise: statement.minimum_value_promise, + viewable_balance, }) }) .transpose()?; - let change = - proof - .change_statement - .as_ref() - .map(|stmt| { - let commitment = Commitment::from_canonical_bytes(&stmt.commitment).map_err(|_| { - ResourceError::InvalidConfidentialProof { - details: "Invalid commitment".to_string(), - } - })?; + let change = proof + .change_statement + .as_ref() + .map(|stmt| { + let commitment = Commitment::from_canonical_bytes(&*stmt.commitment).map_err(|_| { + ResourceError::InvalidConfidentialProof { + details: "Invalid commitment".to_string(), + } + })?; - let stealth_public_nonce = PublicKey::from_canonical_bytes(stmt.sender_public_nonce.as_bytes()) - .map_err(|_| ResourceError::InvalidConfidentialProof { - details: "Invalid sender public nonce".to_string(), - })?; - - Ok(ConfidentialOutput { - commitment, - stealth_public_nonce, - encrypted_data: stmt.encrypted_data.clone(), - minimum_value_promise: stmt.minimum_value_promise, - }) + let stealth_public_nonce = PublicKey::from_canonical_bytes(&*stmt.sender_public_nonce).map_err(|_| { + ResourceError::InvalidConfidentialProof { + details: "Invalid sender public nonce".to_string(), + } + })?; + + let viewable_balance = + validate_elgamal_verifiable_balance_proof(&commitment, view_key, stmt.viewable_balance_proof.as_ref())?; + + Ok(ConfidentialOutput { + commitment, + stealth_public_nonce, + encrypted_data: stmt.encrypted_data.clone(), + minimum_value_promise: stmt.minimum_value_promise, + viewable_balance, }) - .transpose()?; + }) + .transpose()?; validate_bullet_proof(proof)?; @@ -88,13 +106,116 @@ pub fn validate_confidential_proof( }) } +pub fn validate_elgamal_verifiable_balance_proof( + commitment: &Commitment, + view_key: Option<&PublicKey>, + viewable_balance_proof: Option<&ViewableBalanceProof>, +) -> Result, ResourceError> { + // Check that if a view key is provided, then a viewable balance proof is also provided and vice versa + let Some(view_key) = view_key else { + if viewable_balance_proof.is_none() { + return Ok(None); + } + return Err(ResourceError::InvalidConfidentialProof { + details: "ViewableBalanceProof provided for a resource that is not viewable".to_string(), + }); + }; + + let Some(proof) = viewable_balance_proof else { + return Err(ResourceError::InvalidConfidentialProof { + details: "ViewableBalanceProof is required for a viewable resource".to_string(), + }); + }; + + // Decode and check that each field is well-formed + let encrypted = PublicKey::from_canonical_bytes(&*proof.elgamal_encrypted).map_err(|_| { + ResourceError::InvalidConfidentialProof { + details: "Invalid value for E".to_string(), + } + })?; + + let elgamal_public_nonce = PublicKey::from_canonical_bytes(&*proof.elgamal_public_nonce).map_err(|_| { + ResourceError::InvalidConfidentialProof { + details: "Invalid public key for R".to_string(), + } + })?; + + let c_prime = + Commitment::from_canonical_bytes(&*proof.c_prime).map_err(|_| ResourceError::InvalidConfidentialProof { + details: "Invalid commitment for C'".to_string(), + })?; + + let e_prime = + Commitment::from_canonical_bytes(&*proof.e_prime).map_err(|_| ResourceError::InvalidConfidentialProof { + details: "Invalid commitment for E'".to_string(), + })?; + + let r_prime = + PublicKey::from_canonical_bytes(&*proof.r_prime).map_err(|_| ResourceError::InvalidConfidentialProof { + details: "Invalid public key for R'".to_string(), + })?; + + let s_v = PrivateKey::from_canonical_bytes(&*proof.s_v).map_err(|_| ResourceError::InvalidConfidentialProof { + details: "Invalid private key for s_v".to_string(), + })?; + + let s_m = PrivateKey::from_canonical_bytes(&*proof.s_m).map_err(|_| ResourceError::InvalidConfidentialProof { + details: "Invalid private key for s_m".to_string(), + })?; + + let s_r = &PrivateKey::from_canonical_bytes(&*proof.s_r).map_err(|_| ResourceError::InvalidConfidentialProof { + details: "Invalid private key for s_r".to_string(), + })?; + + // Fiat-Shamir challenge + let e = &RistrettoSecretKey::from_uniform_bytes(&challenges::viewable_balance_proof_challenge64( + commitment, + view_key, + proof.as_challenge_fields(), + )) + // TODO: it would be better if from_uniform_bytes took a [u8; 64] + .expect("INVARIANT VIOLATION: RistrettoSecretKey::from_uniform_bytes and hash output length mismatch"); + + // Check eC + C' ?= s_m.G + sv.H + let left = e * commitment.as_public_key() + c_prime.as_public_key(); + let right = get_commitment_factory().commit(&s_m, &s_v); + if left != *right.as_public_key() { + return Err(ResourceError::InvalidConfidentialProof { + details: "Invalid viewable balance proof (eC + C' != s_m.G + s_v.H)".to_string(), + }); + } + + // Check eE + E' ?= s_v.G + s_r.P + let left = e * &encrypted + e_prime.as_public_key(); + let right = PublicKey::from_secret_key(&s_v) + s_r * view_key; + if left != right { + return Err(ResourceError::InvalidConfidentialProof { + details: "Invalid viewable balance proof (eE + E' != s_v.G + s_r.P)".to_string(), + }); + } + + // Check eR + R' ?= s_r.G + let left = e * &elgamal_public_nonce + r_prime; + let right = PublicKey::from_secret_key(s_r); + if left != right { + return Err(ResourceError::InvalidConfidentialProof { + details: "Invalid viewable balance proof (eR + R' != s_r.G)".to_string(), + }); + } + + Ok(Some(ElgamalVerifiableBalance { + encrypted, + public_nonce: elgamal_public_nonce, + })) +} + fn validate_bullet_proof(proof: &ConfidentialOutputProof) -> Result<(), ResourceError> { let statements = proof .output_statement .iter() .chain(proof.change_statement.iter()) .map(|stmt| { - let commitment = Commitment::from_canonical_bytes(&stmt.commitment).map_err(|_| { + let commitment = Commitment::from_canonical_bytes(&*stmt.commitment).map_err(|_| { ResourceError::InvalidConfidentialProof { details: "Invalid commitment".to_string(), } diff --git a/dan_layer/engine_types/src/confidential/value_lookup_table.rs b/dan_layer/engine_types/src/confidential/value_lookup_table.rs new file mode 100644 index 000000000..fe40404ba --- /dev/null +++ b/dan_layer/engine_types/src/confidential/value_lookup_table.rs @@ -0,0 +1,7 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +pub trait ValueLookupTable { + type Error; + fn lookup(&mut self, value: u64) -> Result, Self::Error>; +} diff --git a/dan_layer/engine_types/src/confidential/withdraw.rs b/dan_layer/engine_types/src/confidential/withdraw.rs index b0c695fb4..1c9628968 100644 --- a/dan_layer/engine_types/src/confidential/withdraw.rs +++ b/dan_layer/engine_types/src/confidential/withdraw.rs @@ -8,11 +8,9 @@ use tari_template_lib::{ crypto::BalanceProofSignature, models::{Amount, ConfidentialWithdrawProof, EncryptedData}, }; -#[cfg(feature = "ts")] -use ts_rs::TS; use super::{challenges, get_commitment_factory, validate_confidential_proof}; -use crate::resource_container::ResourceError; +use crate::{confidential::elgamal::ElgamalVerifiableBalance, resource_container::ResourceError}; #[derive(Debug, Clone)] pub struct ValidatedConfidentialWithdrawProof { @@ -31,7 +29,11 @@ pub struct ValidatedConfidentialWithdrawProof { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(feature = "ts", derive(TS), ts(export, export_to = "../../bindings/src/types/"))] +#[cfg_attr( + feature = "ts", + derive(ts_rs::TS), + ts(export, export_to = "../../bindings/src/types/") +)] pub struct ConfidentialOutput { #[cfg_attr(feature = "ts", ts(type = "string"))] pub commitment: Commitment, @@ -41,13 +43,15 @@ pub struct ConfidentialOutput { pub encrypted_data: EncryptedData, #[cfg_attr(feature = "ts", ts(type = "number"))] pub minimum_value_promise: u64, + pub viewable_balance: Option, } -pub fn validate_confidential_withdraw<'a, I: IntoIterator>( +pub(crate) fn validate_confidential_withdraw<'a, I: IntoIterator>( inputs: I, + view_key: Option<&PublicKey>, withdraw_proof: ConfidentialWithdrawProof, ) -> Result { - let validated_proof = validate_confidential_proof(&withdraw_proof.output_proof)?; + let validated_proof = validate_confidential_proof(&withdraw_proof.output_proof, view_key)?; let input_revealed_amount = withdraw_proof.input_revealed_amount; // We expect the revealed amount to be excluded from the output commitment. diff --git a/dan_layer/engine_types/src/hashing.rs b/dan_layer/engine_types/src/hashing.rs index 69f3adbc3..3064aadb1 100644 --- a/dan_layer/engine_types/src/hashing.rs +++ b/dan_layer/engine_types/src/hashing.rs @@ -184,6 +184,7 @@ pub enum EngineHashDomainLabel { FeeClaimAddress, QuorumCertificate, SubstateValue, + ViewKey, } impl EngineHashDomainLabel { @@ -208,6 +209,7 @@ impl EngineHashDomainLabel { Self::FeeClaimAddress => "FeeClaimAddress", Self::QuorumCertificate => "QuorumCertificate", Self::SubstateValue => "SubstateValue", + Self::ViewKey => "ViewKey", } } } diff --git a/dan_layer/engine_types/src/resource.rs b/dan_layer/engine_types/src/resource.rs index 0cc39873b..8574d7bfc 100644 --- a/dan_layer/engine_types/src/resource.rs +++ b/dan_layer/engine_types/src/resource.rs @@ -21,17 +21,20 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use serde::{Deserialize, Serialize}; +use tari_common_types::types::PublicKey; use tari_template_lib::{ auth::{OwnerRule, Ownership, ResourceAccessRules}, crypto::RistrettoPublicKeyBytes, models::{Amount, Metadata}, resource::{ResourceType, TOKEN_SYMBOL}, }; -#[cfg(feature = "ts")] -use ts_rs::TS; #[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "ts", derive(TS), ts(export, export_to = "../../bindings/src/types/"))] +#[cfg_attr( + feature = "ts", + derive(ts_rs::TS), + ts(export, export_to = "../../bindings/src/types/") +)] pub struct Resource { resource_type: ResourceType, owner_rule: OwnerRule, @@ -40,6 +43,8 @@ pub struct Resource { access_rules: ResourceAccessRules, metadata: Metadata, total_supply: Amount, + #[cfg_attr(feature = "ts", ts(type = "string | null"))] + view_key: Option, } impl Resource { @@ -49,6 +54,7 @@ impl Resource { owner_rule: OwnerRule, access_rules: ResourceAccessRules, metadata: Metadata, + view_key: Option, ) -> Self { Self { resource_type, @@ -57,6 +63,7 @@ impl Resource { access_rules, metadata, total_supply: 0.into(), + view_key, } } @@ -79,6 +86,10 @@ impl Resource { } } + pub fn view_key(&self) -> Option<&PublicKey> { + self.view_key.as_ref() + } + pub fn access_rules(&self) -> &ResourceAccessRules { &self.access_rules } diff --git a/dan_layer/engine_types/src/resource_container.rs b/dan_layer/engine_types/src/resource_container.rs index 584d08110..b51ca07a8 100644 --- a/dan_layer/engine_types/src/resource_container.rs +++ b/dan_layer/engine_types/src/resource_container.rs @@ -8,7 +8,7 @@ use std::{collections::BTreeMap, mem}; use serde::{Deserialize, Serialize}; -use tari_common_types::types::Commitment; +use tari_common_types::types::{Commitment, PublicKey}; use tari_crypto::tari_utilities::ByteArray; use tari_template_abi::rust::collections::BTreeSet; use tari_template_lib::{ @@ -92,6 +92,7 @@ impl ResourceContainer { pub fn mint_confidential( address: ResourceAddress, proof: ConfidentialOutputProof, + view_key: Option<&PublicKey>, ) -> Result { if proof.change_statement.is_some() { return Err(ResourceError::InvalidConfidentialMintWithChange); @@ -101,7 +102,7 @@ impl ResourceContainer { details: "Change revealed amount must be zero for minting".to_string(), }); } - let validated_proof = validate_confidential_proof(&proof)?; + let validated_proof = validate_confidential_proof(&proof, view_key)?; assert!( validated_proof.change_output.is_none(), "invariant failed: validate_confidential_proof returned change with no change in input proof" @@ -352,6 +353,7 @@ impl ResourceContainer { pub fn withdraw_confidential( &mut self, proof: ConfidentialWithdrawProof, + view_key: Option<&PublicKey>, ) -> Result { match self { ResourceContainer::Fungible { .. } => Err(ResourceError::OperationNotAllowed( @@ -386,7 +388,7 @@ impl ResourceContainer { }) .collect::, ResourceError>>()?; - let validated_proof = validate_confidential_withdraw(&inputs, proof)?; + let validated_proof = validate_confidential_withdraw(&inputs, view_key, proof)?; // Withdraw revealed amount if validated_proof.input_revealed_amount > *revealed_amount { @@ -486,12 +488,12 @@ impl ResourceContainer { } } - // TODO: remove this as it is exactly the same as withdraw_confidential pub fn reveal_confidential( &mut self, proof: ConfidentialWithdrawProof, + view_key: Option<&PublicKey>, ) -> Result { - self.withdraw_confidential(proof) + self.withdraw_confidential(proof, view_key) } /// Returns all confidential commitments. If the resource is not confidential, None is returned. diff --git a/dan_layer/engine_types/src/vault.rs b/dan_layer/engine_types/src/vault.rs index 48c04cc34..62a0b446e 100644 --- a/dan_layer/engine_types/src/vault.rs +++ b/dan_layer/engine_types/src/vault.rs @@ -23,7 +23,7 @@ use std::collections::{BTreeMap, BTreeSet}; use serde::{Deserialize, Serialize}; -use tari_common_types::types::Commitment; +use tari_common_types::types::{Commitment, PrivateKey, PublicKey}; use tari_template_lib::{ crypto::PedersonCommitmentBytes, models::{Amount, ConfidentialWithdrawProof, NonFungibleId, ResourceAddress, VaultId}, @@ -34,7 +34,7 @@ use ts_rs::TS; use crate::{ bucket::Bucket, - confidential::ConfidentialOutput, + confidential::{ConfidentialOutput, ValueLookupTable}, proof::{ContainerRef, LockedResource, Proof}, resource_container::{ResourceContainer, ResourceError}, }; @@ -71,8 +71,9 @@ impl Vault { pub fn withdraw_confidential( &mut self, proof: ConfidentialWithdrawProof, + view_key: Option<&PublicKey>, ) -> Result { - self.resource_container.withdraw_confidential(proof) + self.resource_container.withdraw_confidential(proof, view_key) } pub fn recall_all(&mut self) -> Result { @@ -104,6 +105,39 @@ impl Vault { self.resource_container.get_confidential_commitments() } + pub fn try_brute_force_confidential_balance( + &self, + secret_view_key: &PrivateKey, + value_range: I, + value_lookup: TValueLookup, + ) -> Result, TValueLookup::Error> + where + I: IntoIterator + Clone, + TValueLookup: ValueLookupTable + Clone, + { + let Some(commitments) = self.get_confidential_commitments() else { + return Ok(None); + }; + + let mut total = 0; + for output in commitments.values() { + let Some(balance) = output.viewable_balance.as_ref() else { + // We assume that if there is no viewable_balance in any (presumably the first) commitment, then this + // resource does not have a view key + return Ok(None); + }; + + let value = balance.brute_force_balance(secret_view_key, value_range.clone(), value_lookup.clone())?; + + match value { + Some(v) => total += v, + // If any of the commitments cannot be brute forced, then we return None + None => return Ok(None), + } + } + Ok(Some(total)) + } + pub fn resource_address(&self) -> &ResourceAddress { self.resource_container.resource_address() } @@ -119,8 +153,9 @@ impl Vault { pub fn reveal_confidential( &mut self, proof: ConfidentialWithdrawProof, + view_key: Option<&PublicKey>, ) -> Result { - self.resource_container.reveal_confidential(proof) + self.resource_container.reveal_confidential(proof, view_key) } pub fn resource_container_mut(&mut self) -> &mut ResourceContainer { diff --git a/dan_layer/p2p/proto/transaction.proto b/dan_layer/p2p/proto/transaction.proto index 7264bfa9e..d6f92d307 100644 --- a/dan_layer/p2p/proto/transaction.proto +++ b/dan_layer/p2p/proto/transaction.proto @@ -115,4 +115,16 @@ message ConfidentialStatement { bytes sender_public_nonce = 2; bytes encrypted_value = 3; uint64 minimum_value_promise = 4; + ViewableBalanceProof viewable_balance_proof = 5; +} + +message ViewableBalanceProof { + bytes elgamal_encrypted = 1; + bytes elgamal_public_nonce = 2; + bytes c_prime = 3; + bytes e_prime = 4; + bytes r_prime = 5; + bytes s_v = 6; + bytes s_m = 7; + bytes s_r = 8; } diff --git a/dan_layer/p2p/src/conversions/transaction.rs b/dan_layer/p2p/src/conversions/transaction.rs index 153215c07..548f99ab1 100644 --- a/dan_layer/p2p/src/conversions/transaction.rs +++ b/dan_layer/p2p/src/conversions/transaction.rs @@ -35,7 +35,14 @@ use tari_engine_types::{ use tari_template_lib::{ args::Arg, crypto::{BalanceProofSignature, PedersonCommitmentBytes, RistrettoPublicKeyBytes}, - models::{ConfidentialOutputProof, ConfidentialStatement, ConfidentialWithdrawProof, EncryptedData, ObjectKey}, + models::{ + ConfidentialOutputProof, + ConfidentialStatement, + ConfidentialWithdrawProof, + EncryptedData, + ObjectKey, + ViewableBalanceProof, + }, }; use tari_transaction::{SubstateRequirement, Transaction}; @@ -500,6 +507,7 @@ impl TryFrom for ConfidentialStatemen .ok_or_else(|| anyhow!("Invalid length of encrypted_value bytes"))?, ), minimum_value_promise: val.minimum_value_promise, + viewable_balance_proof: val.viewable_balance_proof.map(TryInto::try_into).transpose()?, }) } } @@ -511,6 +519,41 @@ impl From for proto::transaction::ConfidentialStatement { sender_public_nonce: val.sender_public_nonce.as_bytes().to_vec(), encrypted_value: val.encrypted_data.as_ref().to_vec(), minimum_value_promise: val.minimum_value_promise, + viewable_balance_proof: val.viewable_balance_proof.map(Into::into), + } + } +} + +// -------------------------------- ViewableBalanceProof -------------------------------- // + +impl TryFrom for ViewableBalanceProof { + type Error = anyhow::Error; + + fn try_from(val: proto::transaction::ViewableBalanceProof) -> Result { + Ok(ViewableBalanceProof { + elgamal_encrypted: val.elgamal_encrypted.as_slice().try_into()?, + elgamal_public_nonce: val.elgamal_public_nonce.as_slice().try_into()?, + c_prime: val.c_prime.as_slice().try_into()?, + e_prime: val.e_prime.as_slice().try_into()?, + r_prime: val.r_prime.as_slice().try_into()?, + s_v: val.s_v.as_slice().try_into()?, + s_m: val.s_m.as_slice().try_into()?, + s_r: val.s_r.as_slice().try_into()?, + }) + } +} + +impl From for proto::transaction::ViewableBalanceProof { + fn from(val: ViewableBalanceProof) -> Self { + Self { + elgamal_encrypted: val.elgamal_encrypted.as_bytes().to_vec(), + elgamal_public_nonce: val.elgamal_public_nonce.as_bytes().to_vec(), + c_prime: val.c_prime.as_bytes().to_vec(), + e_prime: val.e_prime.as_bytes().to_vec(), + r_prime: val.r_prime.as_bytes().to_vec(), + s_v: val.s_v.as_bytes().to_vec(), + s_m: val.s_m.as_bytes().to_vec(), + s_r: val.s_r.as_bytes().to_vec(), } } } diff --git a/dan_layer/p2p/src/utils.rs b/dan_layer/p2p/src/utils.rs index 83caba8b3..0c4a76fec 100644 --- a/dan_layer/p2p/src/utils.rs +++ b/dan_layer/p2p/src/utils.rs @@ -1,11 +1,12 @@ // Copyright 2024 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -pub(crate) fn checked_copy_fixed(bytes: &[u8]) -> Option<[u8; SZ]> { +pub(crate) fn checked_copy_fixed(bytes: &[u8]) -> Option +where [u8; SZ]: Into { if bytes.len() != SZ { return None; } let mut array = [0u8; SZ]; array.copy_from_slice(&bytes[..SZ]); - Some(array) + Some(array.into()) } diff --git a/dan_layer/template_lib/src/args/types.rs b/dan_layer/template_lib/src/args/types.rs index a0e2cd8cd..ae175986b 100644 --- a/dan_layer/template_lib/src/args/types.rs +++ b/dan_layer/template_lib/src/args/types.rs @@ -33,7 +33,7 @@ use ts_rs::TS; use crate::{ args::Arg, auth::{OwnerRule, ResourceAccessRules}, - crypto::PedersonCommitmentBytes, + crypto::{PedersonCommitmentBytes, RistrettoPublicKeyBytes}, models::{ AddressAllocation, Amount, @@ -240,6 +240,16 @@ pub enum MintArg { }, } +impl MintArg { + pub fn as_resource_type(&self) -> ResourceType { + match self { + MintArg::Fungible { .. } => ResourceType::Fungible, + MintArg::NonFungible { .. } => ResourceType::NonFungible, + MintArg::Confidential { .. } => ResourceType::Confidential, + } + } +} + /// A resource creation operation #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CreateResourceArg { @@ -248,6 +258,7 @@ pub struct CreateResourceArg { pub access_rules: ResourceAccessRules, pub metadata: Metadata, pub mint_arg: Option, + pub view_key: Option, } /// A resource minting operation argument diff --git a/dan_layer/template_lib/src/crypto/balance_proof.rs b/dan_layer/template_lib/src/crypto/balance_proof.rs index f919f81b4..8a91e5b08 100644 --- a/dan_layer/template_lib/src/crypto/balance_proof.rs +++ b/dan_layer/template_lib/src/crypto/balance_proof.rs @@ -33,7 +33,7 @@ impl BalanceProofSignature { let mut key = [0u8; Self::length()]; key[..32].copy_from_slice(public_nonce); key[32..].copy_from_slice(signature); - Ok(BalanceProofSignature(key)) + Ok(Self(key)) } pub fn from_bytes(bytes: &[u8]) -> Result { @@ -46,7 +46,7 @@ impl BalanceProofSignature { let mut key = [0u8; Self::length()]; key.copy_from_slice(bytes); - Ok(BalanceProofSignature(key)) + Ok(Self(key)) } pub fn as_public_nonce(&self) -> &[u8] { @@ -73,3 +73,9 @@ impl TryFrom<&[u8]> for BalanceProofSignature { Self::from_bytes(value) } } + +impl AsRef<[u8]> for BalanceProofSignature { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} diff --git a/dan_layer/template_lib/src/crypto/commitment.rs b/dan_layer/template_lib/src/crypto/commitment.rs index be4b00826..f8b448a10 100644 --- a/dan_layer/template_lib/src/crypto/commitment.rs +++ b/dan_layer/template_lib/src/crypto/commitment.rs @@ -3,7 +3,10 @@ use serde::{Deserialize, Serialize}; use serde_with::{serde_as, Bytes}; -use tari_template_abi::rust::fmt::{Display, Formatter}; +use tari_template_abi::rust::{ + fmt::{Display, Formatter}, + ops::Deref, +}; use crate::{crypto::InvalidByteLengthError, Hash}; @@ -54,7 +57,7 @@ impl TryFrom<&[u8]> for PedersonCommitmentBytes { impl AsRef<[u8]> for PedersonCommitmentBytes { fn as_ref(&self) -> &[u8] { - self.as_bytes() + self.deref().as_ref() } } @@ -69,3 +72,11 @@ impl Display for PedersonCommitmentBytes { write!(f, "{}", self.as_hash()) } } + +impl Deref for PedersonCommitmentBytes { + type Target = [u8; 32]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/dan_layer/template_lib/src/crypto/mod.rs b/dan_layer/template_lib/src/crypto/mod.rs index 34eb94cf8..5c5aef406 100644 --- a/dan_layer/template_lib/src/crypto/mod.rs +++ b/dan_layer/template_lib/src/crypto/mod.rs @@ -7,8 +7,10 @@ mod balance_proof; mod commitment; mod error; mod ristretto; +mod schnorr; pub use balance_proof::*; pub use commitment::*; pub use error::*; pub use ristretto::*; +pub use schnorr::*; diff --git a/dan_layer/template_lib/src/crypto/ristretto.rs b/dan_layer/template_lib/src/crypto/ristretto.rs index 95b5d319d..be91a06e5 100644 --- a/dan_layer/template_lib/src/crypto/ristretto.rs +++ b/dan_layer/template_lib/src/crypto/ristretto.rs @@ -3,7 +3,10 @@ use serde::{Deserialize, Serialize}; use serde_with::{serde_as, Bytes}; -use tari_template_abi::rust::fmt::{Display, Formatter}; +use tari_template_abi::rust::{ + fmt::{Display, Formatter}, + ops::Deref, +}; use crate::{crypto::InvalidByteLengthError, models::NonFungibleAddress, Hash}; @@ -58,7 +61,7 @@ impl TryFrom<&[u8]> for RistrettoPublicKeyBytes { impl AsRef<[u8]> for RistrettoPublicKeyBytes { fn as_ref(&self) -> &[u8] { - self.as_bytes() + self.deref().as_ref() } } @@ -73,3 +76,11 @@ impl Display for RistrettoPublicKeyBytes { write!(f, "{}", self.as_hash()) } } + +impl Deref for RistrettoPublicKeyBytes { + type Target = [u8; 32]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/dan_layer/template_lib/src/crypto/schnorr.rs b/dan_layer/template_lib/src/crypto/schnorr.rs new file mode 100644 index 000000000..2a6194f32 --- /dev/null +++ b/dan_layer/template_lib/src/crypto/schnorr.rs @@ -0,0 +1,68 @@ +// Copyright 2023 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, Bytes}; +use tari_template_abi::rust::ops::Deref; + +use crate::crypto::InvalidByteLengthError; + +#[serde_as] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct SchnorrSignatureBytes(#[serde_as(as = "Bytes")] [u8; SchnorrSignatureBytes::length()]); + +impl SchnorrSignatureBytes { + pub const fn length() -> usize { + 32 + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != Self::length() { + return Err(InvalidByteLengthError { + size: bytes.len(), + expected: Self::length(), + }); + } + + let mut key = [0u8; Self::length()]; + key.copy_from_slice(bytes); + Ok(Self(key)) + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn into_array(self) -> [u8; Self::length()] { + self.0 + } +} + +impl TryFrom<&[u8]> for SchnorrSignatureBytes { + type Error = InvalidByteLengthError; + + fn try_from(value: &[u8]) -> Result { + Self::from_bytes(value) + } +} + +impl AsRef<[u8]> for SchnorrSignatureBytes { + fn as_ref(&self) -> &[u8] { + self.deref().as_ref() + } +} + +impl Deref for SchnorrSignatureBytes { + type Target = [u8; Self::length()]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<[u8; SchnorrSignatureBytes::length()]> for SchnorrSignatureBytes { + fn from(bytes: [u8; SchnorrSignatureBytes::length()]) -> Self { + Self(bytes) + } +} diff --git a/dan_layer/template_lib/src/models/amount.rs b/dan_layer/template_lib/src/models/amount.rs index 5dd96b3fa..ada0d11fe 100644 --- a/dan_layer/template_lib/src/models/amount.rs +++ b/dan_layer/template_lib/src/models/amount.rs @@ -106,6 +106,8 @@ impl Amount { Amount(self.0.saturating_div(other.0)) } + /// Returns the value as a u64 if possible, otherwise returns None. + /// Since the internal representation is i64, this will return None if the value is negative. pub fn as_u64_checked(&self) -> Option { self.0.try_into().ok() } diff --git a/dan_layer/template_lib/src/models/confidential_proof.rs b/dan_layer/template_lib/src/models/confidential_proof.rs index 4709f1f37..4ecd228fa 100644 --- a/dan_layer/template_lib/src/models/confidential_proof.rs +++ b/dan_layer/template_lib/src/models/confidential_proof.rs @@ -7,7 +7,7 @@ use serde_with::{serde_as, Bytes}; use ts_rs::TS; use crate::{ - crypto::{BalanceProofSignature, PedersonCommitmentBytes, RistrettoPublicKeyBytes}, + crypto::{BalanceProofSignature, PedersonCommitmentBytes, RistrettoPublicKeyBytes, SchnorrSignatureBytes}, models::Amount, }; @@ -40,12 +40,11 @@ impl ConfidentialOutputProof { } /// A zero-knowledge proof that a confidential resource amount is valid -#[serde_as] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "ts", derive(TS), ts(export, export_to = "../../bindings/src/types/"))] pub struct ConfidentialStatement { - #[serde_as(as = "Bytes")] - pub commitment: [u8; 32], + #[cfg_attr(feature = "ts", ts(type = "Array"))] + pub commitment: PedersonCommitmentBytes, /// Public nonce (R) that was used to generate the commitment mask #[cfg_attr(feature = "ts", ts(type = "Array"))] pub sender_public_nonce: RistrettoPublicKeyBytes, @@ -54,14 +53,58 @@ pub struct ConfidentialStatement { pub encrypted_data: EncryptedData, #[cfg_attr(feature = "ts", ts(type = "number"))] pub minimum_value_promise: u64, + pub viewable_balance_proof: Option, +} + +/// A zero-knowledge proof that a confidential resource amount is valid +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "ts", derive(TS), ts(export, export_to = "../../bindings/src/types/"))] +pub struct ViewableBalanceProof { + /// E = v.G + r.P + #[cfg_attr(feature = "ts", ts(type = "Uint8Array"))] + pub elgamal_encrypted: RistrettoPublicKeyBytes, + #[cfg_attr(feature = "ts", ts(type = "Uint8Array"))] + pub elgamal_public_nonce: RistrettoPublicKeyBytes, + #[cfg_attr(feature = "ts", ts(type = "Uint8Array"))] + pub c_prime: PedersonCommitmentBytes, + #[cfg_attr(feature = "ts", ts(type = "Uint8Array"))] + pub e_prime: PedersonCommitmentBytes, + #[cfg_attr(feature = "ts", ts(type = "Uint8Array"))] + pub r_prime: RistrettoPublicKeyBytes, + #[cfg_attr(feature = "ts", ts(type = "Uint8Array"))] + pub s_v: SchnorrSignatureBytes, + #[cfg_attr(feature = "ts", ts(type = "Uint8Array"))] + pub s_m: SchnorrSignatureBytes, + #[cfg_attr(feature = "ts", ts(type = "Uint8Array"))] + pub s_r: SchnorrSignatureBytes, +} + +impl ViewableBalanceProof { + pub fn as_challenge_fields(&self) -> ViewableBalanceProofChallengeFields<'_> { + ViewableBalanceProofChallengeFields { + elgamal_encrypted: &self.elgamal_encrypted, + elgamal_public_nonce: &self.elgamal_public_nonce, + c_prime: &self.c_prime, + e_prime: &self.e_prime, + r_prime: &self.r_prime, + } + } +} + +#[derive(Clone, Copy, Serialize)] +pub struct ViewableBalanceProofChallengeFields<'a> { + pub elgamal_encrypted: &'a RistrettoPublicKeyBytes, + pub elgamal_public_nonce: &'a RistrettoPublicKeyBytes, + pub c_prime: &'a PedersonCommitmentBytes, + pub e_prime: &'a PedersonCommitmentBytes, + pub r_prime: &'a RistrettoPublicKeyBytes, } /// A zero-knowledge proof that a withdrawal of confidential resources from a vault is valid #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "ts", derive(TS), ts(export, export_to = "../../bindings/src/types/"))] pub struct ConfidentialWithdrawProof { - // #[cfg_attr(feature = "hex", serde(with = "hex::serde"))] - #[cfg_attr(feature = "ts", ts(type = "Array"))] + #[cfg_attr(feature = "ts", ts(type = "Array"))] pub inputs: Vec, /// The amount to withdraw from revealed funds i.e. the revealed funds as inputs #[cfg_attr(feature = "ts", ts(type = "number"))] diff --git a/dan_layer/template_lib/src/models/vault.rs b/dan_layer/template_lib/src/models/vault.rs index 363a9297a..00301171f 100644 --- a/dan_layer/template_lib/src/models/vault.rs +++ b/dan_layer/template_lib/src/models/vault.rs @@ -35,7 +35,7 @@ use tari_template_abi::{ #[cfg(feature = "ts")] use ts_rs::TS; -use super::{BinaryTag, KeyParseError, NonFungible, ObjectKey, Proof, ProofAuth}; +use super::{BinaryTag, EntityId, KeyParseError, NonFungible, ObjectKey, Proof, ProofAuth}; use crate::{ args::{ ConfidentialRevealArg, @@ -73,6 +73,10 @@ impl VaultId { pub fn as_object_key(&self) -> &ObjectKey { self.0.inner() } + + pub fn entity_id(&self) -> EntityId { + self.0.inner().as_entity_id() + } } impl From for VaultId { diff --git a/dan_layer/template_lib/src/resource/builder/confidential.rs b/dan_layer/template_lib/src/resource/builder/confidential.rs index 075a628c7..1f274d675 100644 --- a/dan_layer/template_lib/src/resource/builder/confidential.rs +++ b/dan_layer/template_lib/src/resource/builder/confidential.rs @@ -5,6 +5,7 @@ use super::TOKEN_SYMBOL; use crate::{ args::MintArg, auth::{AccessRule, OwnerRule, ResourceAccessRules}, + crypto::RistrettoPublicKeyBytes, models::{Bucket, Metadata, ResourceAddress}, prelude::ConfidentialOutputProof, resource::{ResourceManager, ResourceType}, @@ -15,6 +16,7 @@ pub struct ConfidentialResourceBuilder { initial_supply_proof: Option, metadata: Metadata, access_rules: ResourceAccessRules, + view_key: Option, owner_rule: OwnerRule, } @@ -25,6 +27,7 @@ impl ConfidentialResourceBuilder { initial_supply_proof: None, metadata: Metadata::new(), access_rules: ResourceAccessRules::new(), + view_key: None, owner_rule: OwnerRule::default(), } } @@ -42,6 +45,14 @@ impl ConfidentialResourceBuilder { self } + /// Specify a view key for the confidential resource. This allows anyone with the secret key to uncover the balance + /// of commitments generated for the resource. + /// NOTE: it is not currently possible to change the view key after the resource is created. + pub fn with_view_key(mut self, view_key: RistrettoPublicKeyBytes) -> Self { + self.view_key = Some(view_key); + self + } + /// Sets up who can mint new tokens of the resource pub fn mintable(mut self, rule: AccessRule) -> Self { self.access_rules = self.access_rules.mintable(rule); @@ -104,7 +115,7 @@ impl ConfidentialResourceBuilder { self.initial_supply_proof.is_none(), "call build_bucket when initial supply is set" ); - let (address, _) = Self::build_internal(self.owner_rule, self.access_rules, self.metadata, None); + let (address, _) = Self::build_internal(self.owner_rule, self.access_rules, self.metadata, None, self.view_key); address } @@ -117,7 +128,13 @@ impl ConfidentialResourceBuilder { ), }; - let (_, bucket) = Self::build_internal(self.owner_rule, self.access_rules, self.metadata, Some(resource)); + let (_, bucket) = Self::build_internal( + self.owner_rule, + self.access_rules, + self.metadata, + Some(resource), + self.view_key, + ); bucket.expect("[build_bucket] Bucket not returned from system") } @@ -126,7 +143,15 @@ impl ConfidentialResourceBuilder { access_rules: ResourceAccessRules, metadata: Metadata, resource: Option, + view_key: Option, ) -> (ResourceAddress, Option) { - ResourceManager::new().create(ResourceType::Confidential, owner_rule, access_rules, metadata, resource) + ResourceManager::new().create( + ResourceType::Confidential, + owner_rule, + access_rules, + metadata, + resource, + view_key, + ) } } diff --git a/dan_layer/template_lib/src/resource/builder/fungible.rs b/dan_layer/template_lib/src/resource/builder/fungible.rs index ec0c5d4d1..edaa63d34 100644 --- a/dan_layer/template_lib/src/resource/builder/fungible.rs +++ b/dan_layer/template_lib/src/resource/builder/fungible.rs @@ -123,6 +123,13 @@ impl FungibleResourceBuilder { metadata: Metadata, mint_arg: Option, ) -> (ResourceAddress, Option) { - ResourceManager::new().create(ResourceType::Fungible, owner_rule, access_rules, metadata, mint_arg) + ResourceManager::new().create( + ResourceType::Fungible, + owner_rule, + access_rules, + metadata, + mint_arg, + None, + ) } } diff --git a/dan_layer/template_lib/src/resource/builder/non_fungible.rs b/dan_layer/template_lib/src/resource/builder/non_fungible.rs index 817961afb..ee350b508 100644 --- a/dan_layer/template_lib/src/resource/builder/non_fungible.rs +++ b/dan_layer/template_lib/src/resource/builder/non_fungible.rs @@ -168,6 +168,13 @@ impl NonFungibleResourceBuilder { metadata: Metadata, resource: Option, ) -> (ResourceAddress, Option) { - ResourceManager::new().create(ResourceType::NonFungible, owner_rule, access_rules, metadata, resource) + ResourceManager::new().create( + ResourceType::NonFungible, + owner_rule, + access_rules, + metadata, + resource, + None, + ) } } diff --git a/dan_layer/template_lib/src/resource/manager.rs b/dan_layer/template_lib/src/resource/manager.rs index a9d67759b..cfa741480 100644 --- a/dan_layer/template_lib/src/resource/manager.rs +++ b/dan_layer/template_lib/src/resource/manager.rs @@ -41,7 +41,7 @@ use crate::{ ResourceUpdateNonFungibleDataArg, }, auth::{OwnerRule, ResourceAccessRules}, - crypto::PedersonCommitmentBytes, + crypto::{PedersonCommitmentBytes, RistrettoPublicKeyBytes}, models::{Amount, Bucket, ConfidentialOutputProof, Metadata, NonFungible, NonFungibleId, ResourceAddress, VaultId}, prelude::ResourceType, }; @@ -102,6 +102,7 @@ impl ResourceManager { access_rules: ResourceAccessRules, metadata: Metadata, mint_arg: Option, + view_key: Option, ) -> (ResourceAddress, Option) { let resp: InvokeResult = call_engine(EngineOp::ResourceInvoke, &ResourceInvokeArg { resource_ref: ResourceRef::Resource, @@ -111,7 +112,8 @@ impl ResourceManager { owner_rule, access_rules, metadata, - mint_arg + mint_arg, + view_key, }], }); diff --git a/dan_layer/template_test_tooling/Cargo.toml b/dan_layer/template_test_tooling/Cargo.toml index f04601033..6685a47a6 100644 --- a/dan_layer/template_test_tooling/Cargo.toml +++ b/dan_layer/template_test_tooling/Cargo.toml @@ -19,6 +19,7 @@ tari_template_builtin = { workspace = true } tari_transaction = { workspace = true } tari_dan_common_types = { workspace = true } tari_bor = { workspace = true, default-features = true } +tari_dan_wallet_crypto = { workspace = true } anyhow = { workspace = true } serde = { workspace = true, features = ["default", "derive"] } diff --git a/dan_layer/template_test_tooling/src/support/confidential.rs b/dan_layer/template_test_tooling/src/support/confidential.rs index 718022823..5c5d90b46 100644 --- a/dan_layer/template_test_tooling/src/support/confidential.rs +++ b/dan_layer/template_test_tooling/src/support/confidential.rs @@ -1,33 +1,35 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -/// These would live in the wallet use rand::rngs::OsRng; -use tari_common_types::types::{BulletRangeProof, PrivateKey, PublicKey, Signature}; -use tari_crypto::{ - commitment::{ExtensionDegree, HomomorphicCommitmentFactory}, - errors::RangeProofError, - extended_range_proof::ExtendedRangeProofService, - keys::{PublicKey as _, SecretKey}, - ristretto::bulletproofs_plus::{RistrettoExtendedMask, RistrettoExtendedWitness}, - tari_utilities::ByteArray, -}; -use tari_engine_types::confidential::{challenges, get_commitment_factory, get_range_proof_service}; +use tari_common_types::types::{PrivateKey, PublicKey}; +use tari_crypto::{commitment::HomomorphicCommitmentFactory, keys::SecretKey, tari_utilities::ByteArray}; +use tari_dan_wallet_crypto::{ConfidentialOutputMaskAndValue, ConfidentialProofStatement}; +use tari_engine_types::confidential::get_commitment_factory; use tari_template_lib::{ - crypto::{BalanceProofSignature, PedersonCommitmentBytes, RistrettoPublicKeyBytes}, - models::{Amount, ConfidentialOutputProof, ConfidentialStatement, ConfidentialWithdrawProof, EncryptedData}, + crypto::PedersonCommitmentBytes, + models::{Amount, ConfidentialOutputProof, ConfidentialWithdrawProof}, }; -pub struct ConfidentialProofStatement { - pub amount: Amount, - pub mask: PrivateKey, - pub sender_public_nonce: PublicKey, - pub minimum_value_promise: u64, +pub fn generate_confidential_proof( + output_amount: Amount, + change: Option, +) -> (ConfidentialOutputProof, PrivateKey, Option) { + generate_confidential_proof_internal(output_amount, change, None) } -pub fn generate_confidential_proof( +pub fn generate_confidential_proof_with_view_key( + output_amount: Amount, + change: Option, + view_key: &PublicKey, +) -> (ConfidentialOutputProof, PrivateKey, Option) { + generate_confidential_proof_internal(output_amount, change, Some(view_key.clone())) +} + +fn generate_confidential_proof_internal( output_amount: Amount, change: Option, + view_key: Option, ) -> (ConfidentialOutputProof, PrivateKey, Option) { let mask = PrivateKey::random(&mut OsRng); let output_statement = ConfidentialProofStatement { @@ -35,6 +37,9 @@ pub fn generate_confidential_proof( mask: mask.clone(), sender_public_nonce: Default::default(), minimum_value_promise: 0, + encrypted_data: Default::default(), + reveal_amount: Default::default(), + resource_view_key: view_key.clone(), }; let change_mask = PrivateKey::random(&mut OsRng); @@ -43,29 +48,16 @@ pub fn generate_confidential_proof( mask: change_mask.clone(), sender_public_nonce: Default::default(), minimum_value_promise: 0, + encrypted_data: Default::default(), + reveal_amount: Default::default(), + resource_view_key: view_key, }); - let proof = generate_confidential_proof_from_statements(output_statement, change_statement).unwrap(); + let proof = + tari_dan_wallet_crypto::create_confidential_proof(&output_statement, change_statement.as_ref()).unwrap(); (proof, mask, change.map(|_| change_mask)) } -pub fn generate_balance_proof( - input_mask: &PrivateKey, - output_mask: &PrivateKey, - change_mask: Option<&PrivateKey>, - input_revealed_amount: Amount, - output_revealed_amount: Amount, -) -> BalanceProofSignature { - let secret_excess = input_mask - output_mask - change_mask.unwrap_or(&PrivateKey::default()); - let excess = PublicKey::from_secret_key(&secret_excess); - let (nonce, public_nonce) = PublicKey::random_keypair(&mut OsRng); - let challenge = - challenges::confidential_withdraw64(&excess, &public_nonce, input_revealed_amount, output_revealed_amount); - - let sig = Signature::sign_raw_uniform(&secret_excess, nonce, &challenge).unwrap(); - BalanceProofSignature::try_from_parts(sig.get_public_nonce().as_bytes(), sig.get_signature().as_bytes()).unwrap() -} - pub struct WithdrawProofOutput { pub output_mask: PrivateKey, pub change_mask: Option, @@ -75,7 +67,7 @@ pub struct WithdrawProofOutput { impl WithdrawProofOutput { pub fn to_commitment_bytes_for_output(&self, amount: Amount) -> PedersonCommitmentBytes { let commitment = get_commitment_factory().commit_value(&self.output_mask, amount.value() as u64); - PedersonCommitmentBytes::from(copy_fixed(commitment.as_bytes())) + PedersonCommitmentBytes::from_bytes(commitment.as_bytes()).unwrap() } } @@ -85,46 +77,16 @@ pub fn generate_withdraw_proof( change_amount: Option, revealed_amount: Amount, ) -> WithdrawProofOutput { - let (output_proof, output_mask, change_mask) = generate_confidential_proof(output_amount, change_amount); let total_amount = output_amount + change_amount.unwrap_or_else(Amount::zero) + revealed_amount; - let input_commitment = get_commitment_factory().commit_value(input_mask, total_amount.value() as u64); - let input_commitment = PedersonCommitmentBytes::from(copy_fixed(input_commitment.as_bytes())); - let balance_proof = generate_balance_proof( - input_mask, - &output_mask, - change_mask.as_ref(), + + generate_withdraw_proof_internal( + &[(input_mask.clone(), total_amount)], Amount::zero(), + output_amount, + change_amount, revealed_amount, - ); - - let output_statement = output_proof.output_statement.map(|o| ConfidentialStatement { - commitment: o.commitment, - sender_public_nonce: Default::default(), - encrypted_data: EncryptedData::default(), - minimum_value_promise: o.minimum_value_promise, - }); - - WithdrawProofOutput { - output_mask, - change_mask, - proof: ConfidentialWithdrawProof { - inputs: vec![input_commitment], - input_revealed_amount: Amount::zero(), - output_proof: ConfidentialOutputProof { - output_statement, - output_revealed_amount: revealed_amount, - change_statement: output_proof.change_statement.map(|statement| ConfidentialStatement { - commitment: statement.commitment, - sender_public_nonce: Default::default(), - encrypted_data: EncryptedData::default(), - minimum_value_promise: statement.minimum_value_promise, - }), - change_revealed_amount: Amount::zero(), - range_proof: output_proof.range_proof, - }, - balance_proof, - }, - } + None, + ) } pub fn generate_withdraw_proof_with_inputs( @@ -134,125 +96,81 @@ pub fn generate_withdraw_proof_with_inputs( change_amount: Option, revealed_output_amount: Amount, ) -> WithdrawProofOutput { - let (output_proof, output_mask, change_mask) = generate_confidential_proof(output_amount, change_amount); - let input_commitments = inputs - .iter() - .map(|(input_mask, amount)| { - let input_commitment = get_commitment_factory().commit_value(input_mask, amount.value() as u64); - PedersonCommitmentBytes::from(copy_fixed(input_commitment.as_bytes())) - }) - .collect(); - let input_private_excess = inputs - .iter() - .fold(PrivateKey::default(), |acc, (input_mask, _)| acc + input_mask); - let balance_proof = generate_balance_proof( - &input_private_excess, - &output_mask, - change_mask.as_ref(), + generate_withdraw_proof_internal( + inputs, input_revealed_amount, + output_amount, + change_amount, revealed_output_amount, - ); - - let output_statement = output_proof.output_statement.map(|o| ConfidentialStatement { - commitment: o.commitment, - // R and encrypted value are informational and can be left out as far as the VN is concerned - sender_public_nonce: Default::default(), - encrypted_data: EncryptedData::default(), - minimum_value_promise: o.minimum_value_promise, - }); - let change_statement = output_proof.change_statement.map(|ch| ConfidentialStatement { - commitment: ch.commitment, - sender_public_nonce: Default::default(), - encrypted_data: EncryptedData::default(), - minimum_value_promise: ch.minimum_value_promise, - }); - - WithdrawProofOutput { - output_mask, - change_mask, - proof: ConfidentialWithdrawProof { - inputs: input_commitments, - input_revealed_amount, - output_proof: ConfidentialOutputProof { - output_statement, - output_revealed_amount: revealed_output_amount, - change_statement, - change_revealed_amount: Amount::zero(), - range_proof: output_proof.range_proof, - }, - balance_proof, - }, - } + None, + ) } -fn copy_fixed(bytes: &[u8]) -> [u8; SZ] { - let mut array = [0u8; SZ]; - array.copy_from_slice(&bytes[..SZ]); - array +pub fn generate_withdraw_proof_with_view_key( + input_mask: &PrivateKey, + input_value: Amount, + output_amount: Amount, + change_amount: Option, + revealed_amount: Amount, + view_key: &PublicKey, +) -> WithdrawProofOutput { + generate_withdraw_proof_internal( + &[(input_mask.clone(), input_value)], + Amount::zero(), + output_amount, + change_amount, + revealed_amount, + Some(view_key.clone()), + ) } -fn generate_confidential_proof_from_statements( - output_statement: ConfidentialProofStatement, - change_statement: Option, -) -> Result { - let output_range_proof = generate_extended_bullet_proof(&output_statement, change_statement.as_ref())?; +fn generate_withdraw_proof_internal( + inputs: &[(PrivateKey, Amount)], + input_revealed_amount: Amount, + output_amount: Amount, + change_amount: Option, + revealed_output_amount: Amount, + view_key: Option, +) -> WithdrawProofOutput { + let output_mask = PrivateKey::random(&mut OsRng); + let change_mask = change_amount.map(|_| PrivateKey::random(&mut OsRng)); - let proof_change_statement = change_statement.map(|statement| ConfidentialStatement { - commitment: commitment_to_bytes(&statement.mask, statement.amount), - sender_public_nonce: RistrettoPublicKeyBytes::from_bytes(statement.sender_public_nonce.as_bytes()) - .expect("[generate_confidential_proof] change nonce"), + let output_proof = ConfidentialProofStatement { + amount: output_amount, + mask: output_mask.clone(), + sender_public_nonce: Default::default(), + minimum_value_promise: 0, encrypted_data: Default::default(), - minimum_value_promise: statement.minimum_value_promise, + reveal_amount: revealed_output_amount, + resource_view_key: view_key.clone(), + }; + let change_proof = change_amount.map(|amount| ConfidentialProofStatement { + amount, + mask: change_mask.clone().unwrap(), + sender_public_nonce: Default::default(), + minimum_value_promise: 0, + encrypted_data: Default::default(), + reveal_amount: Default::default(), + resource_view_key: view_key, }); - Ok(ConfidentialOutputProof { - output_statement: Some(ConfidentialStatement { - commitment: commitment_to_bytes(&output_statement.mask, output_statement.amount), - sender_public_nonce: RistrettoPublicKeyBytes::from_bytes(output_statement.sender_public_nonce.as_bytes()) - .expect("[generate_confidential_proof] output nonce"), - encrypted_data: Default::default(), - minimum_value_promise: output_statement.minimum_value_promise, - }), - output_revealed_amount: Amount::zero(), - change_statement: proof_change_statement, - change_revealed_amount: Amount::zero(), - range_proof: output_range_proof.0, - }) -} - -fn generate_extended_bullet_proof( - output_statement: &ConfidentialProofStatement, - change_statement: Option<&ConfidentialProofStatement>, -) -> Result { - let mut extended_witnesses = vec![]; - - let extended_mask = - RistrettoExtendedMask::assign(ExtensionDegree::DefaultPedersen, vec![output_statement.mask.clone()]).unwrap(); + let proof = tari_dan_wallet_crypto::create_withdraw_proof( + &inputs + .iter() + .map(|(mask, amount)| ConfidentialOutputMaskAndValue { + value: amount.as_u64_checked().unwrap(), + mask: mask.clone(), + }) + .collect::>(), + input_revealed_amount, + &output_proof, + change_proof.as_ref(), + ) + .unwrap(); - let mut agg_factor = 1; - extended_witnesses.push(RistrettoExtendedWitness { - mask: extended_mask, - value: output_statement.amount.value() as u64, - minimum_value_promise: output_statement.minimum_value_promise, - }); - if let Some(stmt) = change_statement { - let extended_mask = - RistrettoExtendedMask::assign(ExtensionDegree::DefaultPedersen, vec![stmt.mask.clone()]).unwrap(); - extended_witnesses.push(RistrettoExtendedWitness { - mask: extended_mask, - value: stmt.amount.value() as u64, - minimum_value_promise: stmt.minimum_value_promise, - }); - agg_factor = 2; + WithdrawProofOutput { + output_mask, + change_mask, + proof, } - - let output_range_proof = get_range_proof_service(agg_factor).construct_extended_proof(extended_witnesses, None)?; - Ok(BulletRangeProof(output_range_proof)) -} - -fn commitment_to_bytes(mask: &PrivateKey, amount: Amount) -> [u8; 32] { - let commitment = get_commitment_factory().commit_value(mask, amount.value() as u64); - let mut bytes = [0u8; 32]; - bytes.copy_from_slice(commitment.as_bytes()); - bytes } diff --git a/dan_layer/template_test_tooling/src/support/mod.rs b/dan_layer/template_test_tooling/src/support/mod.rs index 88bb695aa..309db98a4 100644 --- a/dan_layer/template_test_tooling/src/support/mod.rs +++ b/dan_layer/template_test_tooling/src/support/mod.rs @@ -4,3 +4,4 @@ pub mod assert_error; pub mod confidential; pub mod crypto; +pub mod value_lookup_tables; diff --git a/dan_layer/template_test_tooling/src/support/value_lookup_tables.rs b/dan_layer/template_test_tooling/src/support/value_lookup_tables.rs new file mode 100644 index 000000000..7e5289caf --- /dev/null +++ b/dan_layer/template_test_tooling/src/support/value_lookup_tables.rs @@ -0,0 +1,17 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::convert::Infallible; + +use tari_engine_types::confidential::ValueLookupTable; + +#[derive(Clone)] +pub struct AlwaysMissLookupTable; + +impl ValueLookupTable for AlwaysMissLookupTable { + type Error = Infallible; + + fn lookup(&mut self, _value: u64) -> Result, Self::Error> { + Ok(None) + } +} diff --git a/dan_layer/template_test_tooling/templates/faucet/Cargo.lock b/dan_layer/template_test_tooling/templates/faucet/Cargo.lock index a2c1bcc1d..0041ae4a3 100644 --- a/dan_layer/template_test_tooling/templates/faucet/Cargo.lock +++ b/dan_layer/template_test_tooling/templates/faucet/Cargo.lock @@ -376,7 +376,7 @@ dependencies = [ [[package]] name = "tari_bor" -version = "0.3.0" +version = "0.4.1" dependencies = [ "ciborium", "ciborium-io", @@ -385,7 +385,7 @@ dependencies = [ [[package]] name = "tari_template_abi" -version = "0.3.0" +version = "0.4.1" dependencies = [ "serde", "tari_bor", @@ -393,7 +393,7 @@ dependencies = [ [[package]] name = "tari_template_lib" -version = "0.3.0" +version = "0.4.1" dependencies = [ "newtype-ops", "serde", @@ -405,7 +405,7 @@ dependencies = [ [[package]] name = "tari_template_macros" -version = "0.3.0" +version = "0.4.1" dependencies = [ "proc-macro2", "quote", diff --git a/dan_layer/wallet/crypto/Cargo.toml b/dan_layer/wallet/crypto/Cargo.toml new file mode 100644 index 000000000..46bf7cfb9 --- /dev/null +++ b/dan_layer/wallet/crypto/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "tari_dan_wallet_crypto" +version.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +tari_engine_types = { workspace = true } +tari_template_lib = { workspace = true } +tari_crypto = { workspace = true } +tari_utilities = { workspace = true } +tari_hash_domains = { workspace = true } + +blake2 = { workspace = true } +chacha20poly1305 = { workspace = true } +digest = { workspace = true } +rand = { workspace = true } +thiserror = { workspace = true } +zeroize = { workspace = true } + +[dev-dependencies] +tari_template_test_tooling = { workspace = true } \ No newline at end of file diff --git a/dan_layer/wallet/crypto/src/api.rs b/dan_layer/wallet/crypto/src/api.rs new file mode 100644 index 000000000..9e8d264f0 --- /dev/null +++ b/dan_layer/wallet/crypto/src/api.rs @@ -0,0 +1,174 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause +use chacha20poly1305::aead; +use rand::rngs::OsRng; +use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + keys::{PublicKey as _, SecretKey}, + ristretto::{pedersen::PedersenCommitment, RistrettoPublicKey, RistrettoSchnorr, RistrettoSecretKey}, + tari_utilities::ByteArray, +}; +use tari_engine_types::confidential::{challenges, get_commitment_factory, ConfidentialOutput}; +use tari_template_lib::{ + crypto::{BalanceProofSignature, PedersonCommitmentBytes}, + models::{Amount, ConfidentialOutputProof, ConfidentialWithdrawProof, EncryptedData}, +}; + +use crate::{ + confidential_output::ConfidentialOutputMaskAndValue, + kdfs, + proof::{create_confidential_proof, decrypt_data_and_mask, encrypt_data}, + ConfidentialProofError, + ConfidentialProofStatement, +}; + +pub fn create_withdraw_proof( + inputs: &[ConfidentialOutputMaskAndValue], + input_revealed_amount: Amount, + output_statement: &ConfidentialProofStatement, + change_statement: Option<&ConfidentialProofStatement>, +) -> Result { + let output_proof = create_confidential_proof(output_statement, change_statement)?; + let (input_commitments, agg_input_mask) = inputs.iter().fold( + (Vec::with_capacity(inputs.len()), RistrettoSecretKey::default()), + |(mut commitments, agg_input), input| { + let commitment = get_commitment_factory().commit_value(&input.mask, input.value); + commitments.push( + PedersonCommitmentBytes::from_bytes(commitment.as_bytes()).expect("PedersonCommitment not 32 bytes"), + ); + (commitments, agg_input + &input.mask) + }, + ); + + let output_revealed_amount = output_proof.output_revealed_amount + output_proof.change_revealed_amount; + let balance_proof = generate_balance_proof( + &agg_input_mask, + input_revealed_amount, + &output_statement.mask, + change_statement.as_ref().map(|ch| &ch.mask), + output_revealed_amount, + ); + + let output_statement = output_proof.output_statement; + let change_statement = output_proof.change_statement; + + Ok(ConfidentialWithdrawProof { + inputs: input_commitments, + input_revealed_amount, + output_proof: ConfidentialOutputProof { + output_statement, + change_statement, + range_proof: output_proof.range_proof, + output_revealed_amount: output_proof.output_revealed_amount, + change_revealed_amount: output_proof.change_revealed_amount, + }, + balance_proof, + }) +} + +pub fn encrypt_value_and_mask( + amount: u64, + mask: &RistrettoSecretKey, + public_nonce: &RistrettoPublicKey, + secret: &RistrettoSecretKey, +) -> Result { + let key = kdfs::encrypted_data_dh_kdf_aead(secret, public_nonce); + let commitment = get_commitment_factory().commit_value(mask, amount); + let encrypted_data = encrypt_data(&key, &commitment, amount, mask)?; + Ok(encrypted_data) +} + +pub fn extract_value_and_mask( + encryption_key: &RistrettoSecretKey, + commitment: &PedersenCommitment, + encrypted_data: &EncryptedData, +) -> Result<(u64, RistrettoSecretKey), WalletCryptoError> { + let (value, mask) = decrypt_data_and_mask(encryption_key, commitment, encrypted_data) + .map_err(|e| WalletCryptoError::FailedDecryptData { details: e.to_string() })?; + Ok((value, mask)) +} + +pub fn unblind_output( + output_commitment: &PedersenCommitment, + output_encrypted_value: &EncryptedData, + claim_secret: &RistrettoSecretKey, + reciprocal_public_key: &RistrettoPublicKey, +) -> Result { + let encryption_key = kdfs::encrypted_data_dh_kdf_aead(claim_secret, reciprocal_public_key); + + let (value, mask) = extract_value_and_mask(&encryption_key, output_commitment, output_encrypted_value)?; + let commitment = get_commitment_factory().commit_value(&mask, value); + if *output_commitment == commitment { + Ok(ConfidentialOutputMaskAndValue { value, mask }) + } else { + Err(WalletCryptoError::UnableToOpenCommitment) + } +} + +pub fn create_output_for_dest( + dest_public_key: &RistrettoPublicKey, + amount: Amount, +) -> Result { + let mask = RistrettoSecretKey::random(&mut OsRng); + // FIXME: This allows anyone to subtract the public mask from the commitment and brute force the value + // This is only used for create free test coins + let stealth_public_nonce = RistrettoPublicKey::from_secret_key(&mask); + let amount = amount + .as_u64_checked() + .ok_or_else(|| WalletCryptoError::InvalidArgument { + name: "amount", + details: "[generate_output_for_dest] amount is negative".to_string(), + })?; + let commitment = create_commitment(&mask, amount); + let encrypt_key = kdfs::encrypted_data_dh_kdf_aead(&mask, dest_public_key); + let encrypted_data = encrypt_data(&encrypt_key, &commitment, amount, &mask)?; + + Ok(ConfidentialOutput { + commitment, + stealth_public_nonce, + encrypted_data, + minimum_value_promise: 0, + viewable_balance: None, + }) +} + +fn create_commitment(mask: &RistrettoSecretKey, value: u64) -> PedersenCommitment { + get_commitment_factory().commit_value(mask, value) +} + +fn generate_balance_proof( + input_mask: &RistrettoSecretKey, + input_revealed_amount: Amount, + output_mask: &RistrettoSecretKey, + change_mask: Option<&RistrettoSecretKey>, + output_reveal_amount: Amount, +) -> BalanceProofSignature { + let secret_excess = input_mask - output_mask - change_mask.unwrap_or(&RistrettoSecretKey::default()); + let excess = RistrettoPublicKey::from_secret_key(&secret_excess); + let (nonce, public_nonce) = RistrettoPublicKey::random_keypair(&mut OsRng); + let challenge = + challenges::confidential_withdraw64(&excess, &public_nonce, input_revealed_amount, output_reveal_amount); + + let sig = RistrettoSchnorr::sign_raw_uniform(&secret_excess, nonce, &challenge).unwrap(); + BalanceProofSignature::try_from_parts(sig.get_public_nonce().as_bytes(), sig.get_signature().as_bytes()).unwrap() +} + +#[derive(Debug, thiserror::Error)] +pub enum WalletCryptoError { + #[error("Confidential proof error: {0}")] + ConfidentialProof(#[from] ConfidentialProofError), + #[error("Failed to decrypt data: {details}")] + FailedDecryptData { details: String }, + #[error("Unable to open the commitment")] + UnableToOpenCommitment, + #[error("Invalid argument {name}: {details}")] + InvalidArgument { name: &'static str, details: String }, + #[error("AEAD error: {0}")] + AeadError(aead::Error), +} + +impl From for WalletCryptoError { + fn from(err: aead::Error) -> Self { + WalletCryptoError::AeadError(err) + } +} diff --git a/dan_layer/wallet/crypto/src/byte_utils.rs b/dan_layer/wallet/crypto/src/byte_utils.rs new file mode 100644 index 000000000..0ba63558d --- /dev/null +++ b/dan_layer/wallet/crypto/src/byte_utils.rs @@ -0,0 +1,23 @@ +// Copyright 2023 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +/// Copies a fixed number of bytes from a slice into a fixed-size array and returns T which must define an infallible +/// conversion from the array of the same size. +/// +/// # Panics +/// If the slice is not the expected size, a panic will occur. It is therefore up to the caller to ensure that this is +/// the case. +pub fn copy_fixed(bytes: &[u8]) -> T +where [u8; SZ]: Into { + if bytes.len() != SZ { + panic!( + "INVARIANT VIOLATION: copy_fixed: expected {} bytes, got {}. Output type: {}", + SZ, + bytes.len(), + std::any::type_name::() + ); + } + let mut array = [0u8; SZ]; + array.copy_from_slice(bytes); + array.into() +} diff --git a/dan_layer/wallet/crypto/src/confidential_output.rs b/dan_layer/wallet/crypto/src/confidential_output.rs new file mode 100644 index 000000000..87b08fe66 --- /dev/null +++ b/dan_layer/wallet/crypto/src/confidential_output.rs @@ -0,0 +1,10 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use tari_crypto::ristretto::RistrettoSecretKey; + +#[derive(Debug, Clone)] +pub struct ConfidentialOutputMaskAndValue { + pub value: u64, + pub mask: RistrettoSecretKey, +} diff --git a/dan_layer/wallet/crypto/src/confidential_statement.rs b/dan_layer/wallet/crypto/src/confidential_statement.rs new file mode 100644 index 000000000..293204319 --- /dev/null +++ b/dan_layer/wallet/crypto/src/confidential_statement.rs @@ -0,0 +1,25 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + ristretto::{pedersen::PedersenCommitment, RistrettoPublicKey, RistrettoSecretKey}, +}; +use tari_engine_types::confidential::get_commitment_factory; +use tari_template_lib::models::{Amount, EncryptedData}; + +pub struct ConfidentialProofStatement { + pub amount: Amount, + pub mask: RistrettoSecretKey, + pub sender_public_nonce: RistrettoPublicKey, + pub minimum_value_promise: u64, + pub encrypted_data: EncryptedData, + pub reveal_amount: Amount, + pub resource_view_key: Option, +} + +impl ConfidentialProofStatement { + pub fn to_commitment(&self) -> PedersenCommitment { + get_commitment_factory().commit_value(&self.mask, self.amount.value() as u64) + } +} diff --git a/dan_layer/wallet/sdk/src/confidential/error.rs b/dan_layer/wallet/crypto/src/error.rs similarity index 92% rename from dan_layer/wallet/sdk/src/confidential/error.rs rename to dan_layer/wallet/crypto/src/error.rs index ac3972644..cc2eac042 100644 --- a/dan_layer/wallet/sdk/src/confidential/error.rs +++ b/dan_layer/wallet/crypto/src/error.rs @@ -10,6 +10,8 @@ pub enum ConfidentialProofError { RangeProof(RangeProofError), #[error("Aead error")] AeadError, + #[error("Negative amount")] + NegativeAmount, } impl From for ConfidentialProofError { diff --git a/dan_layer/wallet/sdk/src/confidential/kdfs.rs b/dan_layer/wallet/crypto/src/kdfs.rs similarity index 59% rename from dan_layer/wallet/sdk/src/confidential/kdfs.rs rename to dan_layer/wallet/crypto/src/kdfs.rs index 9dd20b21d..20ac82b28 100644 --- a/dan_layer/wallet/sdk/src/confidential/kdfs.rs +++ b/dan_layer/wallet/crypto/src/kdfs.rs @@ -1,10 +1,13 @@ -// Copyright 2023 The Tari Project +// Copyright 2024 The Tari Project // SPDX-License-Identifier: BSD-3-Clause use chacha20poly1305::aead::generic_array::GenericArray; use digest::FixedOutput; -use tari_common_types::types::{PrivateKey, PublicKey}; -use tari_crypto::{dhke::DiffieHellmanSharedSecret, keys::SecretKey}; +use tari_crypto::{ + dhke::DiffieHellmanSharedSecret, + keys::SecretKey, + ristretto::{RistrettoPublicKey, RistrettoSecretKey}, +}; use tari_engine_types::base_layer_hashing::encrypted_data_hasher; use tari_utilities::{hidden_type, safe_array::SafeArray, Hidden}; use zeroize::Zeroize; @@ -13,13 +16,16 @@ hidden_type!(EncryptedDataKey32, SafeArray); hidden_type!(EncryptedDataKey64, SafeArray); /// Generate a ChaCha20-Poly1305 key from a private key and commitment using Blake2b -pub fn encrypted_data_dh_kdf_aead(private_key: &PrivateKey, public_nonce: &PublicKey) -> PrivateKey { - let shared_secret = DiffieHellmanSharedSecret::::new(private_key, public_nonce); +pub fn encrypted_data_dh_kdf_aead( + private_key: &RistrettoSecretKey, + public_nonce: &RistrettoPublicKey, +) -> RistrettoSecretKey { + let shared_secret = DiffieHellmanSharedSecret::::new(private_key, public_nonce); let mut aead_key = EncryptedDataKey64::from(SafeArray::default()); // Must match base layer burn encrypted_data_hasher() .chain(shared_secret.as_bytes()) .finalize_into(GenericArray::from_mut_slice(aead_key.reveal_mut())); - PrivateKey::from_uniform_bytes(aead_key.reveal()).unwrap() + RistrettoSecretKey::from_uniform_bytes(aead_key.reveal()).unwrap() } diff --git a/dan_layer/wallet/crypto/src/lib.rs b/dan_layer/wallet/crypto/src/lib.rs new file mode 100644 index 000000000..89a0cf481 --- /dev/null +++ b/dan_layer/wallet/crypto/src/lib.rs @@ -0,0 +1,18 @@ +// Copyright 2023 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +mod error; +pub mod kdfs; +mod proof; + +pub use error::ConfidentialProofError; +pub use proof::*; + +mod api; +pub use api::*; +mod byte_utils; +mod confidential_output; +pub use confidential_output::*; + +mod confidential_statement; +pub use confidential_statement::*; diff --git a/dan_layer/wallet/sdk/src/confidential/proof.rs b/dan_layer/wallet/crypto/src/proof.rs similarity index 58% rename from dan_layer/wallet/sdk/src/confidential/proof.rs rename to dan_layer/wallet/crypto/src/proof.rs index 33ead911b..d958ba711 100644 --- a/dan_layer/wallet/sdk/src/confidential/proof.rs +++ b/dan_layer/wallet/crypto/src/proof.rs @@ -1,4 +1,4 @@ -// Copyright 2023 The Tari Project +// Copyright 2024 The Tari Project // SPDX-License-Identifier: BSD-3-Clause use std::mem::size_of; @@ -16,71 +16,44 @@ use chacha20poly1305::{ XNonce, }; use digest::FixedOutput; -use lazy_static::lazy_static; -use tari_common_types::types::{BulletRangeProof, Commitment, CommitmentFactory, PrivateKey, PublicKey}; use tari_crypto::{ commitment::{ExtensionDegree, HomomorphicCommitmentFactory}, errors::RangeProofError, extended_range_proof::ExtendedRangeProofService, hashing::DomainSeparatedHasher, - keys::SecretKey, - ristretto::bulletproofs_plus::{BulletproofsPlusService, RistrettoExtendedMask, RistrettoExtendedWitness}, + keys::{PublicKey, SecretKey}, + ristretto::{ + bulletproofs_plus::{RistrettoExtendedMask, RistrettoExtendedWitness}, + pedersen::PedersenCommitment, + RistrettoPublicKey, + RistrettoSchnorr, + RistrettoSecretKey, + }, tari_utilities::ByteArray, }; +use tari_engine_types::confidential::{challenges, get_commitment_factory, get_range_proof_service}; use tari_hash_domains::TransactionSecureNonceKdfDomain; use tari_template_lib::{ crypto::RistrettoPublicKeyBytes, - models::{Amount, ConfidentialOutputProof, ConfidentialStatement, EncryptedData}, + models::{ + ConfidentialOutputProof, + ConfidentialStatement, + EncryptedData, + ViewableBalanceProof, + ViewableBalanceProofChallengeFields, + }, }; use tari_utilities::safe_array::SafeArray; use zeroize::Zeroizing; use crate::{ byte_utils::copy_fixed, - confidential::{error::ConfidentialProofError, kdfs::EncryptedDataKey32}, + error::ConfidentialProofError, + kdfs::EncryptedDataKey32, + ConfidentialProofStatement, }; -lazy_static! { - /// Static reference to the default commitment factory. Each instance of CommitmentFactory requires a number of heap allocations. - static ref COMMITMENT_FACTORY: CommitmentFactory = CommitmentFactory::default(); - /// Static reference to the default range proof service. Each instance of RangeProofService requires a number of heap allocations. - static ref RANGE_PROOF_AGG_1_SERVICE: BulletproofsPlusService = - BulletproofsPlusService::init(64, 1, CommitmentFactory::default()).unwrap(); - static ref RANGE_PROOF_AGG_2_SERVICE: BulletproofsPlusService = - BulletproofsPlusService::init(64, 2, CommitmentFactory::default()).unwrap(); -} - -pub fn get_range_proof_service(aggregation_factor: usize) -> &'static BulletproofsPlusService { - match aggregation_factor { - 1 => &RANGE_PROOF_AGG_1_SERVICE, - 2 => &RANGE_PROOF_AGG_2_SERVICE, - _ => panic!( - "Unsupported BP aggregation factor {}. Expected 1 or 2", - aggregation_factor - ), - } -} - -pub fn get_commitment_factory() -> &'static CommitmentFactory { - &COMMITMENT_FACTORY -} - -pub struct ConfidentialProofStatement { - pub amount: Amount, - pub mask: PrivateKey, - pub sender_public_nonce: PublicKey, - pub minimum_value_promise: u64, - pub encrypted_data: EncryptedData, - pub reveal_amount: Amount, -} - -impl ConfidentialProofStatement { - pub fn to_commitment(&self) -> Commitment { - get_commitment_factory().commit_value(&self.mask, self.amount.value() as u64) - } -} - -pub fn generate_confidential_proof( +pub fn create_confidential_proof( output_statement: &ConfidentialProofStatement, change_statement: Option<&ConfidentialProofStatement>, ) -> Result { @@ -94,6 +67,14 @@ pub fn generate_confidential_proof( .expect("[generate_confidential_proof] change nonce"), encrypted_data: stmt.encrypted_data.clone(), minimum_value_promise: stmt.minimum_value_promise, + viewable_balance_proof: stmt.resource_view_key.as_ref().map(|view_key| { + create_viewable_balance_proof( + &stmt.mask, + stmt.amount.as_u64_checked().unwrap(), + &change_commitment, + view_key, + ) + }), }) }) .transpose()?; @@ -102,22 +83,32 @@ pub fn generate_confidential_proof( let output_range_proof = generate_extended_bullet_proof(output_statement, change_statement)?; + let output_value = output_statement + .amount + .as_u64_checked() + .ok_or(ConfidentialProofError::NegativeAmount)?; + Ok(ConfidentialOutputProof { output_statement: Some(ConfidentialStatement { commitment: copy_fixed(commitment.as_bytes()), - sender_public_nonce: RistrettoPublicKeyBytes::from_bytes(output_statement.sender_public_nonce.as_bytes()) - .expect("[generate_confidential_proof] output nonce"), + sender_public_nonce: copy_fixed(output_statement.sender_public_nonce.as_bytes()), encrypted_data: output_statement.encrypted_data.clone(), minimum_value_promise: output_statement.minimum_value_promise, + viewable_balance_proof: output_statement.resource_view_key.as_ref().map(|view_key| { + create_viewable_balance_proof(&output_statement.mask, output_value, &commitment, view_key) + }), }), change_statement: proof_change_statement, - range_proof: output_range_proof.0, + range_proof: output_range_proof, output_revealed_amount: output_statement.reveal_amount, change_revealed_amount: change_statement.map(|stmt| stmt.reveal_amount).unwrap_or_default(), }) } -fn inner_encrypted_data_kdf_aead(encryption_key: &PrivateKey, commitment: &Commitment) -> EncryptedDataKey32 { +fn inner_encrypted_data_kdf_aead( + encryption_key: &RistrettoSecretKey, + commitment: &PedersenCommitment, +) -> EncryptedDataKey32 { let mut aead_key = EncryptedDataKey32::from(SafeArray::default()); DomainSeparatedHasher::, TransactionSecureNonceKdfDomain>::new_with_label("encrypted_value_and_mask") .chain(encryption_key.as_bytes()) @@ -126,19 +117,87 @@ fn inner_encrypted_data_kdf_aead(encryption_key: &PrivateKey, commitment: &Commi aead_key } +pub fn create_viewable_balance_proof( + mask: &RistrettoSecretKey, + output_amount: u64, + commitment: &PedersenCommitment, + view_key: &RistrettoPublicKey, +) -> ViewableBalanceProof { + let (elgamal_secret_nonce, elgamal_public_nonce) = RistrettoPublicKey::random_keypair(&mut OsRng); + let r = &elgamal_secret_nonce; + let value_as_secret = RistrettoSecretKey::from(output_amount); + + // E = v.G + rP + let elgamal_encrypted = RistrettoPublicKey::from_secret_key(&value_as_secret) + r * view_key; + + // Nonces + let x_v = RistrettoSecretKey::random(&mut OsRng); + let x_m = RistrettoSecretKey::random(&mut OsRng); + let x_r = RistrettoSecretKey::random(&mut OsRng); + + // C' = x_m.G + x_v.H + let c_prime = get_commitment_factory().commit(&x_m, &x_v); + // E' = x_v.G + x_r.P + let e_prime = RistrettoPublicKey::from_secret_key(&x_v) + &x_r * view_key; + // R' = x_r.G + let r_prime = RistrettoPublicKey::from_secret_key(&x_r); + + // Create challenge + let elgamal_encrypted = copy_fixed(elgamal_encrypted.as_bytes()); + let elgamal_public_nonce = copy_fixed(elgamal_public_nonce.as_bytes()); + let c_prime = copy_fixed(c_prime.as_bytes()); + let e_prime = copy_fixed(e_prime.as_bytes()); + let r_prime = copy_fixed(r_prime.as_bytes()); + + let challenge_fields = ViewableBalanceProofChallengeFields { + elgamal_encrypted: &elgamal_encrypted, + elgamal_public_nonce: &elgamal_public_nonce, + c_prime: &c_prime, + e_prime: &e_prime, + r_prime: &r_prime, + }; + + let e = &challenges::viewable_balance_proof_challenge64(commitment, view_key, challenge_fields); + + // Generate signatures + // TODO: sign_raw_uniform should take a [u8; 64] for the challenge so that length mismatches are caught at compile + // time. The challenge is never a secret (in all current usages), so non-zeroed memory is not an issue. + + // sv = ev + x_v + let s_v = RistrettoSchnorr::sign_raw_uniform(&value_as_secret, x_v, e) + .expect("INVARIANT VIOLATION: sv RistrettoSchnorr::sign_raw_uniform and challenge hash output length mismatch"); + // sm = em + x_m + let s_m = RistrettoSchnorr::sign_raw_uniform(mask, x_m, e) + .expect("INVARIANT VIOLATION: sm RistrettoSchnorr::sign_raw_uniform and challenge hash output length mismatch"); + // sr = er + x_r + let s_r = RistrettoSchnorr::sign_raw_uniform(r, x_r, e) + .expect("INVARIANT VIOLATION: sr RistrettoSchnorr::sign_raw_uniform and challenge hash output length mismatch"); + + ViewableBalanceProof { + elgamal_encrypted, + elgamal_public_nonce, + c_prime, + e_prime, + r_prime, + s_v: copy_fixed(s_v.get_signature().as_bytes()), + s_m: copy_fixed(s_m.get_signature().as_bytes()), + s_r: copy_fixed(s_r.get_signature().as_bytes()), + } +} + const ENCRYPTED_DATA_TAG: &[u8] = b"TARI_AAD_VALUE_AND_MASK_EXTEND_NONCE_VARIANT"; // Useful size constants, each in bytes const SIZE_NONCE: usize = size_of::(); const SIZE_VALUE: usize = size_of::(); -const SIZE_MASK: usize = PrivateKey::KEY_LEN; +const SIZE_MASK: usize = RistrettoSecretKey::KEY_LEN; const SIZE_TAG: usize = size_of::(); const SIZE_TOTAL: usize = SIZE_NONCE + SIZE_VALUE + SIZE_MASK + SIZE_TAG; pub(crate) fn encrypt_data( - encryption_key: &PrivateKey, - commitment: &Commitment, + encryption_key: &RistrettoSecretKey, + commitment: &PedersenCommitment, value: u64, - mask: &PrivateKey, + mask: &RistrettoSecretKey, ) -> Result { // Encode the value and mask let mut bytes = Zeroizing::new([0u8; SIZE_VALUE + SIZE_MASK]); @@ -165,10 +224,10 @@ pub(crate) fn encrypt_data( } pub fn decrypt_data_and_mask( - encryption_key: &PrivateKey, - commitment: &Commitment, + encryption_key: &RistrettoSecretKey, + commitment: &PedersenCommitment, encrypted_data: &EncryptedData, -) -> Result<(u64, PrivateKey), aead::Error> { +) -> Result<(u64, RistrettoSecretKey), aead::Error> { // Extract the nonce, ciphertext, and tag let nonce = XNonce::from_slice(&encrypted_data.0.as_bytes()[..SIZE_NONCE]); let mut bytes = Zeroizing::new([0u8; SIZE_VALUE + SIZE_MASK]); @@ -187,14 +246,15 @@ pub fn decrypt_data_and_mask( value_bytes.clone_from_slice(&bytes[0..SIZE_VALUE]); Ok(( u64::from_le_bytes(value_bytes), - PrivateKey::from_canonical_bytes(&bytes[SIZE_VALUE..]).expect("The length of bytes is exactly SIZE_MASK"), + RistrettoSecretKey::from_canonical_bytes(&bytes[SIZE_VALUE..]) + .expect("The length of bytes is exactly SIZE_MASK"), )) } fn generate_extended_bullet_proof( output_statement: &ConfidentialProofStatement, change_statement: Option<&ConfidentialProofStatement>, -) -> Result { +) -> Result, RangeProofError> { let mut extended_witnesses = vec![]; let extended_mask = @@ -218,25 +278,25 @@ fn generate_extended_bullet_proof( } let output_range_proof = get_range_proof_service(agg_factor).construct_extended_proof(extended_witnesses, None)?; - Ok(BulletRangeProof(output_range_proof)) + Ok(output_range_proof) } #[cfg(test)] mod tests { use rand::rngs::OsRng; - use tari_common_types::types::PrivateKey; - use tari_crypto::keys::SecretKey; + use tari_crypto::{keys::SecretKey, ristretto::RistrettoSecretKey}; use tari_engine_types::confidential::validate_confidential_proof; use tari_template_lib::models::Amount; use super::*; mod confidential_proof { + use super::*; fn create_valid_proof(amount: Amount, minimum_value_promise: u64) -> ConfidentialOutputProof { - let mask = PrivateKey::random(&mut OsRng); - generate_confidential_proof( + let mask = RistrettoSecretKey::random(&mut OsRng); + create_confidential_proof( &ConfidentialProofStatement { amount, minimum_value_promise, @@ -244,6 +304,7 @@ mod tests { sender_public_nonce: Default::default(), reveal_amount: Default::default(), encrypted_data: EncryptedData([0u8; EncryptedData::size()]), + resource_view_key: None, }, None, ) @@ -253,28 +314,30 @@ mod tests { #[test] fn it_is_valid_if_proof_is_valid() { let proof = create_valid_proof(100.into(), 0); - validate_confidential_proof(&proof).unwrap(); + validate_confidential_proof(&proof, None).unwrap(); } #[test] fn it_is_invalid_if_minimum_value_changed() { let mut proof = create_valid_proof(100.into(), 100); proof.output_statement.as_mut().unwrap().minimum_value_promise = 99; - validate_confidential_proof(&proof).unwrap_err(); + validate_confidential_proof(&proof, None).unwrap_err(); proof.output_statement.as_mut().unwrap().minimum_value_promise = 1000; - validate_confidential_proof(&proof).unwrap_err(); + validate_confidential_proof(&proof, None).unwrap_err(); } } mod encrypt_decrypt { + use tari_crypto::ristretto::RistrettoSecretKey; + use super::*; #[test] fn it_encrypts_and_decrypts() { - let key = PrivateKey::random(&mut OsRng); + let key = RistrettoSecretKey::random(&mut OsRng); let amount = 100; let commitment = get_commitment_factory().commit_value(&key, amount); - let mask = PrivateKey::random(&mut OsRng); + let mask = RistrettoSecretKey::random(&mut OsRng); let encrypted = encrypt_data(&key, &commitment, amount, &mask).unwrap(); let val = decrypt_data_and_mask(&key, &commitment, &encrypted).unwrap(); diff --git a/dan_layer/wallet/crypto/tests/viewable_balance_proof.rs b/dan_layer/wallet/crypto/tests/viewable_balance_proof.rs new file mode 100644 index 000000000..49889d8f3 --- /dev/null +++ b/dan_layer/wallet/crypto/tests/viewable_balance_proof.rs @@ -0,0 +1,101 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::time::Instant; + +use rand::rngs::OsRng; +use tari_crypto::{ + keys::{PublicKey, SecretKey}, + ristretto::{pedersen::PedersenCommitment, RistrettoPublicKey, RistrettoSecretKey}, +}; +use tari_dan_wallet_crypto::{create_confidential_proof, ConfidentialProofStatement}; +use tari_engine_types::confidential::validate_elgamal_verifiable_balance_proof; +use tari_template_lib::models::Amount; +use tari_template_test_tooling::support::value_lookup_tables::AlwaysMissLookupTable; +use tari_utilities::ByteArray; + +fn create_output_statement(value: Amount, view_key: &RistrettoPublicKey) -> ConfidentialProofStatement { + let mask = RistrettoSecretKey::random(&mut OsRng); + ConfidentialProofStatement { + amount: value, + mask, + sender_public_nonce: Default::default(), + minimum_value_promise: 0, + encrypted_data: Default::default(), + reveal_amount: Default::default(), + resource_view_key: Some(view_key.clone()), + } +} + +fn keypair_from_seed(seed: u8) -> (RistrettoSecretKey, RistrettoPublicKey) { + let secret_key = RistrettoSecretKey::from_canonical_bytes(&[seed; 32]).unwrap(); + let public_key = RistrettoPublicKey::from_secret_key(&secret_key); + (secret_key, public_key) +} + +#[test] +fn it_allows_no_balance_proof_for_no_view_key() { + let commitment = PedersenCommitment::from_public_key(&RistrettoPublicKey::default()); + let proof = validate_elgamal_verifiable_balance_proof(&commitment, None, None).unwrap(); + assert!(proof.is_none()); +} + +#[test] +fn it_errors_no_balance_proof_with_view_key() { + let (_, view_key) = keypair_from_seed(1); + let output_statement = create_output_statement(123.into(), &view_key); + + let proof = create_confidential_proof(&output_statement, None).unwrap(); + let output_statement = proof.output_statement.as_ref().unwrap(); + let viewable_balance_proof = proof + .output_statement + .as_ref() + .unwrap() + .viewable_balance_proof + .as_ref() + .unwrap(); + let commitment = PedersenCommitment::from_canonical_bytes(output_statement.commitment.as_ref()).unwrap(); + validate_elgamal_verifiable_balance_proof(&commitment, None, Some(viewable_balance_proof)).unwrap_err(); +} + +#[test] +fn it_errors_with_balance_proof_and_no_view_key() { + let commitment = PedersenCommitment::from_public_key(&RistrettoPublicKey::default()); + validate_elgamal_verifiable_balance_proof(&commitment, Some(&RistrettoPublicKey::default()), None).unwrap_err(); +} + +#[test] +fn it_generates_a_valid_proof() { + let (view_key_secret, view_key) = keypair_from_seed(1); + let output_statement = create_output_statement(123.into(), &view_key); + + let timer = Instant::now(); + let proof = create_confidential_proof(&output_statement, None).unwrap(); + let gen_proof_time = timer.elapsed(); + + let output_statement = proof.output_statement.as_ref().unwrap(); + let viewable_balance_proof = proof + .output_statement + .as_ref() + .unwrap() + .viewable_balance_proof + .as_ref() + .unwrap(); + let commitment = PedersenCommitment::from_canonical_bytes(output_statement.commitment.as_ref()).unwrap(); + let timer = Instant::now(); + let proof = validate_elgamal_verifiable_balance_proof(&commitment, Some(&view_key), Some(viewable_balance_proof)) + .unwrap() + .unwrap(); + let validate_proof_time = timer.elapsed(); + + let timer = Instant::now(); + let balance = proof + .brute_force_balance(&view_key_secret, 0..=1000, AlwaysMissLookupTable) + .unwrap(); + let brute_force_time = timer.elapsed(); + assert_eq!(balance, Some(123)); + + println!("Generate proof time: {:?}", gen_proof_time); + println!("Validate proof time: {:?}", validate_proof_time); + println!("Brute force time: {:?}", brute_force_time); +} diff --git a/dan_layer/wallet/sdk/Cargo.toml b/dan_layer/wallet/sdk/Cargo.toml index b859240be..75d4735f4 100644 --- a/dan_layer/wallet/sdk/Cargo.toml +++ b/dan_layer/wallet/sdk/Cargo.toml @@ -13,9 +13,9 @@ tari_common_types = { workspace = true } tari_crypto = { workspace = true, features = ["borsh"] } tari_engine_types = { workspace = true } tari_dan_common_types = { workspace = true } +tari_dan_wallet_crypto = { workspace = true } # Just used for QuorumCertificate tari_dan_storage = { workspace = true } -tari_hash_domains = { workspace = true } tari_key_manager = { workspace = true } tari_transaction = { workspace = true } tari_template_lib = { workspace = true } @@ -24,18 +24,14 @@ tari_utilities = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } -chacha20poly1305 = { workspace = true } blake2 = { workspace = true } chrono = { workspace = true } digest = { workspace = true } jsonwebtoken = { workspace = true } -lazy_static = { workspace = true } log = { workspace = true } -rand = { workspace = true } serde = { workspace = true, default-features = true } serde_json = { workspace = true } thiserror = { workspace = true } -zeroize = { workspace = true } ts-rs = { workspace = true, optional = true } [dev-dependencies] diff --git a/dan_layer/wallet/sdk/src/apis/confidential_crypto.rs b/dan_layer/wallet/sdk/src/apis/confidential_crypto.rs index 06e7715f4..86876662c 100644 --- a/dan_layer/wallet/sdk/src/apis/confidential_crypto.rs +++ b/dan_layer/wallet/sdk/src/apis/confidential_crypto.rs @@ -1,33 +1,22 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use chacha20poly1305::aead; -use rand::rngs::OsRng; -use tari_common_types::types::{Commitment, PrivateKey, PublicKey, Signature}; -use tari_crypto::{ - commitment::HomomorphicCommitmentFactory, - keys::{PublicKey as _, SecretKey}, - tari_utilities::ByteArray, -}; -use tari_engine_types::confidential::{challenges, ConfidentialOutput}; -use tari_template_lib::{ - crypto::{BalanceProofSignature, PedersonCommitmentBytes}, - models::{Amount, ConfidentialOutputProof, ConfidentialWithdrawProof, EncryptedData}, -}; - -use crate::{ - byte_utils::copy_fixed, - confidential::{ - decrypt_data_and_mask, - encrypt_data, - generate_confidential_proof, - get_commitment_factory, - kdfs, - ConfidentialProofError, - ConfidentialProofStatement, - }, - models::ConfidentialOutputWithMask, +use tari_common_types::types::{Commitment, PrivateKey, PublicKey}; +use tari_dan_wallet_crypto::{ + create_confidential_proof, + create_output_for_dest, + create_withdraw_proof, + encrypt_value_and_mask, + extract_value_and_mask, + kdfs, + unblind_output, + ConfidentialOutputMaskAndValue, + ConfidentialProofError, + ConfidentialProofStatement, + WalletCryptoError, }; +use tari_engine_types::confidential::ConfidentialOutput; +use tari_template_lib::models::{Amount, ConfidentialOutputProof, ConfidentialWithdrawProof, EncryptedData}; pub struct ConfidentialCryptoApi; @@ -46,45 +35,13 @@ impl ConfidentialCryptoApi { pub fn generate_withdraw_proof( &self, - inputs: &[ConfidentialOutputWithMask], + inputs: &[ConfidentialOutputMaskAndValue], input_revealed_amount: Amount, output_statement: &ConfidentialProofStatement, change_statement: Option<&ConfidentialProofStatement>, ) -> Result { - let output_proof = generate_confidential_proof(output_statement, change_statement)?; - let input_commitments = inputs - .iter() - .map(|input| PedersonCommitmentBytes::from(copy_fixed(input.commitment.as_bytes()))) - .collect(); - - let agg_input_mask = inputs - .iter() - .fold(PrivateKey::default(), |acc, output| acc + &output.mask); - - let output_revealed_amount = output_proof.output_revealed_amount + output_proof.change_revealed_amount; - let balance_proof = generate_balance_proof( - &agg_input_mask, - input_revealed_amount, - &output_statement.mask, - change_statement.as_ref().map(|ch| &ch.mask), - output_revealed_amount, - ); - - let output_statement = output_proof.output_statement; - let change_statement = output_proof.change_statement; - - Ok(ConfidentialWithdrawProof { - inputs: input_commitments, - input_revealed_amount: Amount::zero(), - output_proof: ConfidentialOutputProof { - output_statement, - change_statement, - range_proof: output_proof.range_proof, - output_revealed_amount: output_proof.output_revealed_amount, - change_revealed_amount: output_proof.change_revealed_amount, - }, - balance_proof, - }) + let proof = create_withdraw_proof(inputs, input_revealed_amount, output_statement, change_statement)?; + Ok(proof) } pub fn encrypt_value_and_mask( @@ -94,10 +51,8 @@ impl ConfidentialCryptoApi { public_nonce: &PublicKey, secret: &PrivateKey, ) -> Result { - let key = kdfs::encrypted_data_dh_kdf_aead(secret, public_nonce); - let commitment = get_commitment_factory().commit_value(mask, amount); - let encrypted_data = encrypt_data(&key, &commitment, amount, mask)?; - Ok(encrypted_data) + let data = encrypt_value_and_mask(amount, mask, public_nonce, secret)?; + Ok(data) } pub fn extract_value_and_mask( @@ -106,16 +61,15 @@ impl ConfidentialCryptoApi { commitment: &Commitment, encrypted_data: &EncryptedData, ) -> Result<(u64, PrivateKey), ConfidentialCryptoApiError> { - let (value, mask) = decrypt_data_and_mask(encryption_key, commitment, encrypted_data) - .map_err(|e| ConfidentialCryptoApiError::FailedDecryptData { details: e.to_string() })?; - Ok((value, mask)) + let value_and_mask = extract_value_and_mask(encryption_key, commitment, encrypted_data)?; + Ok(value_and_mask) } pub fn generate_output_proof( &self, statement: &ConfidentialProofStatement, ) -> Result { - let proof = generate_confidential_proof(statement, None)?; + let proof = create_confidential_proof(statement, None)?; Ok(proof) } @@ -125,21 +79,14 @@ impl ConfidentialCryptoApi { output_encrypted_value: &EncryptedData, claim_secret: &PrivateKey, reciprocal_public_key: &PublicKey, - ) -> Result { - let encryption_key = self.derive_encrypted_data_key_for_receiver(reciprocal_public_key, claim_secret); - - let (value, mask) = self.extract_value_and_mask(&encryption_key, output_commitment, output_encrypted_value)?; - let commitment = get_commitment_factory().commit_value(&mask, value); - if *output_commitment == commitment { - Ok(ConfidentialOutputWithMask { - commitment, - value, - mask, - public_asset_tag: None, - }) - } else { - Err(ConfidentialCryptoApiError::UnableToOpenCommitment) - } + ) -> Result { + let unmasked_output = unblind_output( + output_commitment, + output_encrypted_value, + claim_secret, + reciprocal_public_key, + )?; + Ok(unmasked_output) } pub fn generate_output_for_dest( @@ -147,64 +94,15 @@ impl ConfidentialCryptoApi { dest_public_key: &PublicKey, amount: Amount, ) -> Result { - let mask = PrivateKey::random(&mut OsRng); - let stealth_public_nonce = PublicKey::from_secret_key(&mask); - let amount = amount - .as_u64_checked() - .ok_or_else(|| ConfidentialCryptoApiError::InvalidArgument { - name: "amount", - details: "[generate_output_for_dest] amount is negative".to_string(), - })?; - let commitment = self.create_commitment(&mask, amount); - let encrypt_key = self.derive_encrypted_data_key_for_receiver(dest_public_key, &mask); - let encrypted_data = encrypt_data(&encrypt_key, &commitment, amount, &mask)?; - - Ok(ConfidentialOutput { - commitment, - stealth_public_nonce, - encrypted_data, - minimum_value_promise: 0, - }) - } - - fn create_commitment(&self, mask: &PrivateKey, value: u64) -> Commitment { - get_commitment_factory().commit_value(mask, value) + let output = create_output_for_dest(dest_public_key, amount)?; + Ok(output) } } -fn generate_balance_proof( - input_mask: &PrivateKey, - input_revealed_amount: Amount, - output_mask: &PrivateKey, - change_mask: Option<&PrivateKey>, - output_reveal_amount: Amount, -) -> BalanceProofSignature { - let secret_excess = input_mask - output_mask - change_mask.unwrap_or(&PrivateKey::default()); - let excess = PublicKey::from_secret_key(&secret_excess); - let (nonce, public_nonce) = PublicKey::random_keypair(&mut OsRng); - let challenge = - challenges::confidential_withdraw64(&excess, &public_nonce, input_revealed_amount, output_reveal_amount); - - let sig = Signature::sign_raw_uniform(&secret_excess, nonce, &challenge).unwrap(); - BalanceProofSignature::try_from_parts(sig.get_public_nonce().as_bytes(), sig.get_signature().as_bytes()).unwrap() -} - #[derive(Debug, thiserror::Error)] pub enum ConfidentialCryptoApiError { + #[error(transparent)] + WalletCryptoError(#[from] WalletCryptoError), #[error("Confidential proof error: {0}")] - ConfidentialProof(#[from] ConfidentialProofError), - #[error("Failed to decrypt data: {details}")] - FailedDecryptData { details: String }, - #[error("Unable to open the commitment")] - UnableToOpenCommitment, - #[error("Invalid argument {name}: {details}")] - InvalidArgument { name: &'static str, details: String }, - #[error("AEAD error: {0}")] - AeadError(aead::Error), -} - -impl From for ConfidentialCryptoApiError { - fn from(err: aead::Error) -> Self { - ConfidentialCryptoApiError::AeadError(err) - } + ConfidentialProofError(#[from] ConfidentialProofError), } diff --git a/dan_layer/wallet/sdk/src/apis/confidential_outputs.rs b/dan_layer/wallet/sdk/src/apis/confidential_outputs.rs index 19c657625..22bbaed59 100644 --- a/dan_layer/wallet/sdk/src/apis/confidential_outputs.rs +++ b/dan_layer/wallet/sdk/src/apis/confidential_outputs.rs @@ -4,6 +4,7 @@ use log::*; use tari_common_types::types::PublicKey; use tari_dan_common_types::optional::{IsNotFoundError, Optional}; +use tari_dan_wallet_crypto::{kdfs, ConfidentialOutputMaskAndValue}; use tari_engine_types::{confidential::ConfidentialOutput, substate::SubstateId}; use tari_key_manager::key_manager::DerivedKey; use tari_template_lib::models::Amount; @@ -16,8 +17,7 @@ use crate::{ key_manager, key_manager::{KeyManagerApi, KeyManagerApiError}, }, - confidential::{kdfs, ConfidentialProofError}, - models::{Account, ConfidentialOutputModel, ConfidentialOutputWithMask, ConfidentialProofId, OutputStatus}, + models::{Account, ConfidentialOutputModel, ConfidentialProofId, OutputStatus}, storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, }; @@ -119,7 +119,7 @@ impl<'a, TStore: WalletStore> ConfidentialOutputsApi<'a, TStore> { &self, outputs: Vec, key_branch: &str, - ) -> Result, ConfidentialOutputsApiError> { + ) -> Result, ConfidentialOutputsApiError> { let mut outputs_with_masks = Vec::with_capacity(outputs.len()); for output in outputs { let output_key = self @@ -146,11 +146,9 @@ impl<'a, TStore: WalletStore> ConfidentialOutputsApi<'a, TStore> { &output.encrypted_data, )?; - outputs_with_masks.push(ConfidentialOutputWithMask { - commitment: output.commitment, + outputs_with_masks.push(ConfidentialOutputMaskAndValue { value: output.value, mask, - public_asset_tag: None, }); } Ok(outputs_with_masks) @@ -263,8 +261,6 @@ impl<'a, TStore: WalletStore> ConfidentialOutputsApi<'a, TStore> { pub enum ConfidentialOutputsApiError { #[error("Store error: {0}")] StoreError(#[from] WalletStorageError), - #[error("Confidential proof error: {0}")] - ConfidentialProof(#[from] ConfidentialProofError), #[error("Confidential crypto error: {0}")] ConfidentialCrypto(#[from] ConfidentialCryptoApiError), #[error("Insufficient funds")] diff --git a/dan_layer/wallet/sdk/src/byte_utils.rs b/dan_layer/wallet/sdk/src/byte_utils.rs deleted file mode 100644 index 5aa1e5c87..000000000 --- a/dan_layer/wallet/sdk/src/byte_utils.rs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2023 The Tari Project -// SPDX-License-Identifier: BSD-3-Clause - -pub fn copy_fixed(bytes: &[u8]) -> [u8; SZ] { - let mut array = [0u8; SZ]; - array.copy_from_slice(&bytes[..SZ]); - array -} diff --git a/dan_layer/wallet/sdk/src/confidential/mod.rs b/dan_layer/wallet/sdk/src/confidential/mod.rs deleted file mode 100644 index 10fcd3153..000000000 --- a/dan_layer/wallet/sdk/src/confidential/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2023 The Tari Project -// SPDX-License-Identifier: BSD-3-Clause -mod error; -pub mod kdfs; -mod proof; - -pub use error::ConfidentialProofError; -pub(crate) use proof::{decrypt_data_and_mask, encrypt_data, generate_confidential_proof}; -pub use proof::{get_commitment_factory, ConfidentialProofStatement}; diff --git a/dan_layer/wallet/sdk/src/lib.rs b/dan_layer/wallet/sdk/src/lib.rs index 77fe8f102..75388525e 100644 --- a/dan_layer/wallet/sdk/src/lib.rs +++ b/dan_layer/wallet/sdk/src/lib.rs @@ -4,8 +4,6 @@ pub mod storage; pub mod apis; -mod byte_utils; -pub mod confidential; pub mod models; mod sdk; diff --git a/dan_layer/wallet/sdk/src/models/confidential_output.rs b/dan_layer/wallet/sdk/src/models/confidential_output.rs index c8108f811..125d36635 100644 --- a/dan_layer/wallet/sdk/src/models/confidential_output.rs +++ b/dan_layer/wallet/sdk/src/models/confidential_output.rs @@ -3,7 +3,7 @@ use std::str::FromStr; -use tari_common_types::types::{Commitment, PrivateKey, PublicKey}; +use tari_common_types::types::{Commitment, PublicKey}; use tari_engine_types::substate::SubstateId; use tari_template_lib::models::EncryptedData; @@ -23,15 +23,6 @@ pub struct ConfidentialOutputModel { pub locked_by_proof: Option, } -// TODO: Better name? -#[derive(Debug, Clone)] -pub struct ConfidentialOutputWithMask { - pub commitment: Commitment, - pub value: u64, - pub mask: PrivateKey, - pub public_asset_tag: Option, -} - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum OutputStatus { /// The output is available for spending diff --git a/dan_layer/wallet/sdk/tests/confidential_output_api.rs b/dan_layer/wallet/sdk/tests/confidential_output_api.rs index f4bde9e2e..b0c904c6b 100644 --- a/dan_layer/wallet/sdk/tests/confidential_output_api.rs +++ b/dan_layer/wallet/sdk/tests/confidential_output_api.rs @@ -8,7 +8,6 @@ use tari_common_types::types::Commitment; use tari_crypto::commitment::HomomorphicCommitmentFactory; use tari_dan_common_types::optional::Optional; use tari_dan_wallet_sdk::{ - confidential::get_commitment_factory, models::{ConfidentialOutputModel, ConfidentialProofId, OutputStatus}, network::{SubstateQueryResult, TransactionQueryResult, WalletNetworkInterface}, storage::{WalletStore, WalletStoreReader}, @@ -16,7 +15,7 @@ use tari_dan_wallet_sdk::{ WalletSdkConfig, }; use tari_dan_wallet_storage_sqlite::SqliteWalletStore; -use tari_engine_types::substate::SubstateId; +use tari_engine_types::{confidential::get_commitment_factory, substate::SubstateId}; use tari_template_abi::TemplateDef; use tari_template_lib::{ constants::CONFIDENTIAL_TARI_RESOURCE_ADDRESS, diff --git a/utilities/generate_ristretto_value_lookup/.gitignore b/utilities/generate_ristretto_value_lookup/.gitignore new file mode 100644 index 000000000..0d57f06fa --- /dev/null +++ b/utilities/generate_ristretto_value_lookup/.gitignore @@ -0,0 +1 @@ +value_lookup.bin \ No newline at end of file diff --git a/utilities/generate_ristretto_value_lookup/Cargo.toml b/utilities/generate_ristretto_value_lookup/Cargo.toml new file mode 100644 index 000000000..4587a1a92 --- /dev/null +++ b/utilities/generate_ristretto_value_lookup/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "generate_ristretto_value_lookup" +version.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +tari_crypto = { workspace = true } + +clap = { workspace = true, features = ["derive"] } +human_bytes = { version = "0.4.3", default-features = false } \ No newline at end of file diff --git a/utilities/generate_ristretto_value_lookup/src/cli.rs b/utilities/generate_ristretto_value_lookup/src/cli.rs new file mode 100644 index 000000000..408112077 --- /dev/null +++ b/utilities/generate_ristretto_value_lookup/src/cli.rs @@ -0,0 +1,27 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::path::PathBuf; + +use clap::Parser; + +const DEFAULT_OUTPUT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/value_lookup.bin"); + +#[derive(Debug, Parser)] +pub struct Cli { + /// Path to output the lookup file + #[clap(short = 'o', long, default_value = DEFAULT_OUTPUT)] + pub output_file: PathBuf, + /// The minimum value to include in the lookup table + #[clap(short = 'm', long, default_value = "0")] + pub min: u64, + /// The maximum value to include in the lookup table + #[clap(short = 'x', long)] + pub max: u64, +} + +impl Cli { + pub fn init() -> Self { + Self::parse() + } +} diff --git a/utilities/generate_ristretto_value_lookup/src/main.rs b/utilities/generate_ristretto_value_lookup/src/main.rs new file mode 100644 index 000000000..84ed81348 --- /dev/null +++ b/utilities/generate_ristretto_value_lookup/src/main.rs @@ -0,0 +1,77 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::{ + fs, + io, + io::{stdout, Write}, + time::Instant, +}; + +use human_bytes::human_bytes; +use tari_crypto::{ + keys::PublicKey, + ristretto::{RistrettoPublicKey, RistrettoSecretKey}, + tari_utilities::ByteArray, +}; + +use crate::cli::Cli; +mod cli; + +fn main() -> io::Result<()> { + let cli = Cli::init(); + let dest_file = cli.output_file; + + let file_size = (cli.max - cli.min + 1) * 32 + 20; + println!( + "Generating Ristretto value lookup table from {} to {} and writing to {} ({})", + cli.min, + cli.max, + dest_file.display(), + human_bytes(file_size as f64) + ); + + println!(); + + let writer = fs::File::create(&dest_file)?; + + let timer = Instant::now(); + write_output(writer, cli.min, cli.max)?; + let elapsed = timer.elapsed(); + + println!(); + + let metadata = fs::metadata(&dest_file)?; + + println!( + "Output written to {} ({}) in {:.2?}", + dest_file.display(), + human_bytes(metadata.len() as f64), + elapsed + ); + + Ok(()) +} + +fn write_output(mut writer: W, min: u64, max: u64) -> io::Result<()> { + // Write header VLKP || min_value (8 bytes) || max_value (8 bytes) + writer.write_all(b"VLKP")?; + writer.write_all(&min.to_be_bytes())?; + writer.write_all(&max.to_be_bytes())?; + + let mut dot_count = 0; + for v in min..=max { + let p = RistrettoPublicKey::from_secret_key(&RistrettoSecretKey::from(v)); + writer.write_all(p.as_bytes())?; + if v % 10000 == 0 { + dot_count += 1; + print!("."); + stdout().flush()?; + } + if dot_count == 80 { + dot_count = 0; + println!(); + } + } + Ok(()) +}