From c0c793c16101621118bd3db2dcd37aec4f28af95 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Fri, 15 Mar 2024 15:39:27 +0400 Subject: [PATCH] feat(engine)!: add Elgamal verifiable encryption (optional view key) support (#976) Description --- feat(engine)!: add Elgamal verifiable encryption (optional view key) support feat: implement brute force value decryption for confidential outputs using secret view key refactor: move confidential wallet crypto into new crate refactor(wallet/sdk): use wallet crypto crate refactor(test tooling): use wallet crypto crate tests(engine): new test checking confidential transfers with a view key enabled tests(wallet/crypto): checks that proof generation, verification and decryption are valid feat(engine): add simple interface for value lookup table Motivation and Context --- Allow a template author to specify a view key on confidential resources optionally. This allows anyone with the secret key to uncover the balance of commitments generated for the resource. ```rust let coins = ResourceBuilder::confidential() .initial_supply(confidential_proof) .with_view_key(view_key) .build_bucket(); ``` Wallets MUST generate a ViewableBalanceProof for all confidential outputs for the resource, allowing validators to verify that the encrypted balance was generated correctly without revealing the balance. All confidential crypto was duplicated in the test tooling and wallet SDK. Since duplicating the Elgamal verifiable encryption scheme could lead to issues down the road, or is just plain ugly, this PR puts all confidential crypto into a crate that is used by the test tooling and wallet SDK. A value lookup table is passed into the brute force function. A production implementation of may make use of the binary file provided by the new generate_ristretto_value_lookup bin crate to return canonical (compressed) bytes for a value. The implementation can optimise for sequential reads and a low memory footprint. A binary file containing 1 billion entries (assuming 6 decimals, whole values from 0 - 1000) will be 32 x 1B in size (32Gb). A future PR will add wallet support for attempting to reveal the balance of vaults. How Has This Been Tested? --- New unit tests What process can a PR reviewer use to test or verify this change? --- Create a template using the view key in a resource. Check transfers work as before. Fetch the commitments from a vault and reveal the balance (this is a manual process currently without wallet support) Breaking Changes --- - [ ] None - [ ] Requires data directory to be deleted - [x] Other - Please specify BREAKING CHANGES: added field to resource create args, meaning any template using this would need to be recompiled. --- Cargo.lock | 49 ++- Cargo.toml | 3 + .../tari_dan_wallet_daemon/Cargo.toml | 1 + .../src/handlers/accounts.rs | 14 +- .../src/handlers/confidential.rs | 29 +- .../src/json_rpc/json_encoding.rs | 1 + .../tari_validator_node/src/bootstrap.rs | 2 + dan_layer/engine/src/bootstrap.rs | 2 + dan_layer/engine/src/runtime/impl.rs | 200 ++++++++---- dan_layer/engine/src/runtime/tracker_auth.rs | 9 +- dan_layer/engine/src/runtime/working_state.rs | 3 +- dan_layer/engine/tests/confidential.rs | 105 ++++++- .../templates/confidential/faucet/src/lib.rs | 17 ++ dan_layer/engine_types/src/bucket.rs | 12 +- .../engine_types/src/confidential/elgamal.rs | 137 +++++++++ .../engine_types/src/confidential/mod.rs | 15 +- .../engine_types/src/confidential/proof.rs | 17 +- .../src/confidential/validation.rs | 187 ++++++++++-- .../src/confidential/value_lookup_table.rs | 7 + .../engine_types/src/confidential/withdraw.rs | 16 +- dan_layer/engine_types/src/hashing.rs | 2 + dan_layer/engine_types/src/resource.rs | 17 +- .../engine_types/src/resource_container.rs | 12 +- dan_layer/engine_types/src/vault.rs | 43 ++- dan_layer/p2p/proto/transaction.proto | 12 + dan_layer/p2p/src/conversions/transaction.rs | 45 ++- dan_layer/p2p/src/utils.rs | 5 +- dan_layer/template_lib/src/args/types.rs | 13 +- .../template_lib/src/crypto/balance_proof.rs | 10 +- .../template_lib/src/crypto/commitment.rs | 15 +- dan_layer/template_lib/src/crypto/mod.rs | 2 + .../template_lib/src/crypto/ristretto.rs | 15 +- dan_layer/template_lib/src/crypto/schnorr.rs | 68 +++++ dan_layer/template_lib/src/models/amount.rs | 2 + .../src/models/confidential_proof.rs | 55 +++- dan_layer/template_lib/src/models/vault.rs | 6 +- .../src/resource/builder/confidential.rs | 31 +- .../src/resource/builder/fungible.rs | 9 +- .../src/resource/builder/non_fungible.rs | 9 +- .../template_lib/src/resource/manager.rs | 6 +- dan_layer/template_test_tooling/Cargo.toml | 1 + .../src/support/confidential.rs | 286 +++++++----------- .../template_test_tooling/src/support/mod.rs | 1 + .../src/support/value_lookup_tables.rs | 17 ++ .../templates/faucet/Cargo.lock | 8 +- dan_layer/wallet/crypto/Cargo.toml | 24 ++ dan_layer/wallet/crypto/src/api.rs | 174 +++++++++++ dan_layer/wallet/crypto/src/byte_utils.rs | 23 ++ .../wallet/crypto/src/confidential_output.rs | 10 + .../crypto/src/confidential_statement.rs | 25 ++ .../src/confidential => crypto/src}/error.rs | 2 + .../src/confidential => crypto/src}/kdfs.rs | 18 +- dan_layer/wallet/crypto/src/lib.rs | 18 ++ .../src/confidential => crypto/src}/proof.rs | 205 ++++++++----- .../crypto/tests/viewable_balance_proof.rs | 101 +++++++ dan_layer/wallet/sdk/Cargo.toml | 6 +- .../sdk/src/apis/confidential_crypto.rs | 174 +++-------- .../sdk/src/apis/confidential_outputs.rs | 12 +- dan_layer/wallet/sdk/src/byte_utils.rs | 8 - dan_layer/wallet/sdk/src/confidential/mod.rs | 9 - dan_layer/wallet/sdk/src/lib.rs | 2 - .../sdk/src/models/confidential_output.rs | 11 +- .../sdk/tests/confidential_output_api.rs | 3 +- .../.gitignore | 1 + .../Cargo.toml | 13 + .../src/cli.rs | 27 ++ .../src/main.rs | 77 +++++ 67 files changed, 1843 insertions(+), 616 deletions(-) create mode 100644 dan_layer/engine_types/src/confidential/elgamal.rs create mode 100644 dan_layer/engine_types/src/confidential/value_lookup_table.rs create mode 100644 dan_layer/template_lib/src/crypto/schnorr.rs create mode 100644 dan_layer/template_test_tooling/src/support/value_lookup_tables.rs create mode 100644 dan_layer/wallet/crypto/Cargo.toml create mode 100644 dan_layer/wallet/crypto/src/api.rs create mode 100644 dan_layer/wallet/crypto/src/byte_utils.rs create mode 100644 dan_layer/wallet/crypto/src/confidential_output.rs create mode 100644 dan_layer/wallet/crypto/src/confidential_statement.rs rename dan_layer/wallet/{sdk/src/confidential => crypto/src}/error.rs (92%) rename dan_layer/wallet/{sdk/src/confidential => crypto/src}/kdfs.rs (59%) create mode 100644 dan_layer/wallet/crypto/src/lib.rs rename dan_layer/wallet/{sdk/src/confidential => crypto/src}/proof.rs (58%) create mode 100644 dan_layer/wallet/crypto/tests/viewable_balance_proof.rs delete mode 100644 dan_layer/wallet/sdk/src/byte_utils.rs delete mode 100644 dan_layer/wallet/sdk/src/confidential/mod.rs create mode 100644 utilities/generate_ristretto_value_lookup/.gitignore create mode 100644 utilities/generate_ristretto_value_lookup/Cargo.toml create mode 100644 utilities/generate_ristretto_value_lookup/src/cli.rs create mode 100644 utilities/generate_ristretto_value_lookup/src/main.rs 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(()) +}