diff --git a/applications/tari_app_grpc/proto/wallet.proto b/applications/tari_app_grpc/proto/wallet.proto index 53d851720d..68e70d6097 100644 --- a/applications/tari_app_grpc/proto/wallet.proto +++ b/applications/tari_app_grpc/proto/wallet.proto @@ -105,6 +105,7 @@ message CreateBurnTransactionRequest{ uint64 amount = 1; uint64 fee_per_gram = 2; string message = 3; + bytes claim_public_key = 4; } @@ -137,6 +138,9 @@ message CreateBurnTransactionResponse{ uint64 transaction_id = 1; bool is_success = 2; string failure_message = 3; + bytes commitment = 4; + bytes ownership_proof = 5; + bytes rangeproof = 6; } message TransferResult { diff --git a/applications/tari_console_wallet/src/automation/commands.rs b/applications/tari_console_wallet/src/automation/commands.rs index d3d446ff6b..89dbb8ada4 100644 --- a/applications/tari_console_wallet/src/automation/commands.rs +++ b/applications/tari_console_wallet/src/automation/commands.rs @@ -136,9 +136,16 @@ pub async fn burn_tari( message: String, ) -> Result { wallet_transaction_service - .burn_tari(amount, UtxoSelectionCriteria::default(), fee_per_gram * uT, message) + .burn_tari( + amount, + UtxoSelectionCriteria::default(), + fee_per_gram * uT, + message, + None, + ) .await .map_err(CommandError::TransactionServiceError) + .map(|res| res.0) } /// publishes a tari-SHA atomic swap HTLC transaction diff --git a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs index 973d4e30da..06d59bc593 100644 --- a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -599,23 +599,34 @@ impl wallet_server::Wallet for WalletGrpcServer { UtxoSelectionCriteria::default(), message.fee_per_gram.into(), message.message, + if message.claim_public_key.is_empty() { + None + } else { + Some( + PublicKey::from_bytes(&message.claim_public_key) + .map_err(|e| Status::invalid_argument(e.to_string()))?, + ) + }, ) .await { - Ok(tx_id) => { + Ok((tx_id, commitment, ownership_proof, rangeproof)) => { debug!(target: LOG_TARGET, "Transaction broadcast: {}", tx_id,); CreateBurnTransactionResponse { transaction_id: tx_id.as_u64(), is_success: true, failure_message: Default::default(), + commitment: commitment.to_vec(), + ownership_proof: ownership_proof.map(|o| o.to_vec()).unwrap_or_default(), + rangeproof: rangeproof.to_vec(), } }, Err(e) => { warn!(target: LOG_TARGET, "Failed to burn Tarid: {}", e); CreateBurnTransactionResponse { - transaction_id: Default::default(), is_success: false, failure_message: e.to_string(), + ..Default::default() } }, }; diff --git a/base_layer/wallet/src/lib.rs b/base_layer/wallet/src/lib.rs index 6be3201d64..9fd74ce0f5 100644 --- a/base_layer/wallet/src/lib.rs +++ b/base_layer/wallet/src/lib.rs @@ -20,6 +20,7 @@ pub mod test_utils; pub mod transaction_service; pub mod types; +use tari_crypto::{hash::blake2::Blake256, hash_domain, hashing::DomainSeparatedHasher}; pub use types::WalletHasher; // For use externally to the code base pub mod util; pub mod wallet; @@ -54,3 +55,6 @@ pub type WalletSqlite = Wallet< ContactsServiceSqliteDatabase, KeyManagerSqliteDatabase, >; + +hash_domain!(BurntOutputDomain, "burnt_output", 1); +type BurntOutputDomainHasher = DomainSeparatedHasher; diff --git a/base_layer/wallet/src/transaction_service/error.rs b/base_layer/wallet/src/transaction_service/error.rs index 11c3ab67d8..8823798040 100644 --- a/base_layer/wallet/src/transaction_service/error.rs +++ b/base_layer/wallet/src/transaction_service/error.rs @@ -34,6 +34,7 @@ use tari_core::transactions::{ transaction_components::{EncryptionError, TransactionError}, transaction_protocol::TransactionProtocolError, }; +use tari_crypto::signatures::CommitmentSignatureError; use tari_p2p::services::liveness::error::LivenessError; use tari_service_framework::reply_channel::TransportChannelError; use tari_utilities::ByteArrayError; @@ -178,6 +179,8 @@ pub enum TransactionServiceError { EncryptionError(#[from] EncryptionError), #[error("FixedHash size error: `{0}`")] FixedHashSizeError(#[from] FixedHashSizeError), + #[error("Commitment signature error: {0}")] + CommitmentSignatureError(#[from] CommitmentSignatureError), } #[derive(Debug, Error)] diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 637226c69c..ed087fa167 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -31,7 +31,7 @@ use chrono::NaiveDateTime; use tari_common_types::{ tari_address::TariAddress, transaction::{ImportStatus, TxId}, - types::{PublicKey, Signature}, + types::{BulletRangeProof, Commitment, PublicKey, Signature}, }; use tari_comms::types::CommsPublicKey; use tari_core::{ @@ -42,6 +42,7 @@ use tari_core::{ transaction_components::{OutputFeatures, Transaction, TransactionOutput}, }, }; +use tari_crypto::ristretto::RistrettoComSig; use tari_service_framework::reply_channel::SenderService; use tokio::sync::broadcast; use tower::Service; @@ -86,6 +87,7 @@ pub enum TransactionServiceRequest { selection_criteria: UtxoSelectionCriteria, fee_per_gram: MicroTari, message: String, + claim_public_key: Option, }, RegisterValidatorNode { amount: MicroTari, @@ -235,6 +237,12 @@ impl fmt::Display for TransactionServiceRequest { #[derive(Debug)] pub enum TransactionServiceResponse { TransactionSent(TxId), + BurntTransactionSent { + tx_id: TxId, + commitment: Commitment, + ownership_proof: Option, + rangeproof: BulletRangeProof, + }, TransactionCancelled, PendingInboundTransactions(HashMap), PendingOutboundTransactions(HashMap), @@ -519,7 +527,8 @@ impl TransactionServiceHandle { selection_criteria: UtxoSelectionCriteria, fee_per_gram: MicroTari, message: String, - ) -> Result { + claim_public_key: Option, + ) -> Result<(TxId, Commitment, Option, BulletRangeProof), TransactionServiceError> { match self .handle .call(TransactionServiceRequest::BurnTari { @@ -527,10 +536,16 @@ impl TransactionServiceHandle { selection_criteria, fee_per_gram, message, + claim_public_key, }) .await?? { - TransactionServiceResponse::TransactionSent(tx_id) => Ok(tx_id), + TransactionServiceResponse::BurntTransactionSent { + tx_id, + commitment, + ownership_proof, + rangeproof, + } => Ok((tx_id, commitment, ownership_proof, rangeproof)), _ => Err(TransactionServiceError::UnexpectedApiResponse), } } diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 40975f25c2..bd928b2ea2 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -36,7 +36,7 @@ use sha2::Sha256; use tari_common_types::{ tari_address::TariAddress, transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, - types::{PrivateKey, PublicKey, Signature}, + types::{Commitment, FixedHash, PrivateKey, PublicKey, RangeProof, Signature}, }; use tari_comms::types::{CommsDHKE, CommsPublicKey}; use tari_comms_dht::outbound::OutboundMessageRequester; @@ -69,12 +69,14 @@ use tari_core::{ use tari_crypto::{ commitment::HomomorphicCommitmentFactory, keys::{PublicKey as PKtrait, SecretKey}, + ristretto::RistrettoComSig, tari_utilities::ByteArray, }; use tari_p2p::domain_message::DomainMessage; use tari_script::{inputs, one_sided_payment_script, script, stealth_payment_script, TariScript}; use tari_service_framework::{reply_channel, reply_channel::Receiver}; use tari_shutdown::ShutdownSignal; +use tari_utilities::hex::Hex; use tokio::{ sync::{mpsc, mpsc::Sender, oneshot, Mutex}, task::JoinHandle, @@ -129,6 +131,7 @@ use crate::{ watch::Watch, }, utxo_scanner_service::RECOVERY_KEY, + BurntOutputDomainHasher, OperationId, }; @@ -648,16 +651,25 @@ where selection_criteria, fee_per_gram, message, + claim_public_key, } => self .burn_tari( amount, selection_criteria, fee_per_gram, message, + claim_public_key, transaction_broadcast_join_handles, ) .await - .map(TransactionServiceResponse::TransactionSent), + .map(|(tx_id, commitment, ownership_proof, rangeproof)| { + TransactionServiceResponse::BurntTransactionSent { + tx_id, + commitment, + ownership_proof, + rangeproof, + } + }), TransactionServiceRequest::RegisterValidatorNode { amount, validator_node_public_key, @@ -1362,20 +1374,24 @@ where .await } - /// Creates a transaction to burn some Tari - /// # Arguments - /// 'amount': The amount of Tari to send to the recipient - /// 'fee_per_gram': The amount of fee per transaction gram to be included in transaction + /// Creates a transaction to burn some Tari. The optional _claim public key_ parameter is used in the challenge of + /// the + // corresponding optional _ownership proof_ return value. Burn commitments and ownership proofs will exclusively be + // used in the 2nd layer (DAN layer). When such an _ownership proof_ is presented later on as part of some + // transaction metadata, the _claim public key_ can be revealed to enable verification of the _ownership proof_ + // and the transaction can be signed with the private key corresponding to the claim public key. + #[allow(clippy::too_many_lines)] pub async fn burn_tari( &mut self, amount: MicroTari, selection_criteria: UtxoSelectionCriteria, fee_per_gram: MicroTari, message: String, + claim_public_key: Option, transaction_broadcast_join_handles: &mut FuturesUnordered< JoinHandle>>, >, - ) -> Result { + ) -> Result<(TxId, Commitment, Option, RangeProof), TransactionServiceError> { let tx_id = TxId::new_random(); let output_features = OutputFeatures::create_burn_output(); // Prepare sender part of the transaction @@ -1407,16 +1423,52 @@ where .await .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; let sender_message = TransactionSenderMessage::new_single_round_message(stp.get_single_round_message()?); + // TODO: Save spending key in key manager let spend_key = PrivateKey::random(&mut OsRng); let rtp = ReceiverTransactionProtocol::new( sender_message, PrivateKey::random(&mut OsRng), - spend_key, + spend_key.clone(), &self.resources.factories, ); let recipient_reply = rtp.get_signed_data()?.clone(); - + let commitment = recipient_reply.output.commitment.clone(); + let range_proof = recipient_reply.output.proof.clone(); + let nonce_a = PrivateKey::random(&mut OsRng); + let nonce_x = PrivateKey::random(&mut OsRng); + let pub_nonce = self.resources.factories.commitment.commit(&nonce_x, &nonce_a); + let mut ownership_proof = None; + + if let Some(claim_public_key) = claim_public_key { + let hasher = BurntOutputDomainHasher::new_with_label("commitment_signature") + .chain(pub_nonce.as_bytes()) + .chain(commitment.as_bytes()) + .chain(claim_public_key.as_bytes()); + + let challenge: FixedHash = digest::Digest::finalize(hasher).into(); + + warn!(target: LOG_TARGET, "Pub nonce: {}", pub_nonce.to_vec().to_hex()); + warn!( + target: LOG_TARGET, + "claim_public_key: {}", + claim_public_key.to_vec().to_hex() + ); + warn!(target: LOG_TARGET, "Challenge: {}", challenge.to_vec().to_hex()); + ownership_proof = Some(RistrettoComSig::sign( + &PrivateKey::from(amount), + &spend_key, + &nonce_a, + &nonce_x, + challenge.as_bytes(), + &*self.resources.factories.commitment, + )?); + warn!( + target: LOG_TARGET, + "Ownership proof: {}", + ownership_proof.clone().unwrap().to_vec().to_hex() + ); + } // Start finalizing stp.add_single_recipient_info(recipient_reply) @@ -1466,7 +1518,7 @@ where ), )?; - Ok(tx_id) + Ok((tx_id, commitment, ownership_proof, range_proof)) } pub async fn register_validator_node( diff --git a/comms/dht/src/store_forward/store.rs b/comms/dht/src/store_forward/store.rs index 70690bde94..c8a91b9d00 100644 --- a/comms/dht/src/store_forward/store.rs +++ b/comms/dht/src/store_forward/store.rs @@ -219,7 +219,7 @@ where S: Service + Se ); let service = self.next_service.ready_oneshot().await?; - return service.oneshot(message).await; + service.oneshot(message).await } async fn get_storage_priority(&self, message: &DecryptedDhtMessage) -> SafResult> { diff --git a/integration_tests/tests/cucumber.rs b/integration_tests/tests/cucumber.rs index 25c69eda88..e780ddf89d 100644 --- a/integration_tests/tests/cucumber.rs +++ b/integration_tests/tests/cucumber.rs @@ -3815,11 +3815,13 @@ async fn node_reached_sync(world: &mut TariWorld, node: String) { #[when(expr = "I create a burn transaction of {int} uT from {word} at fee {int}")] async fn burn_transaction(world: &mut TariWorld, amount: u64, wallet: String, fee: u64) { let mut client = world.get_wallet_client(&wallet).await.unwrap(); + let identity = client.identify(GetIdentityRequest {}).await.unwrap().into_inner(); let req = grpc::CreateBurnTransactionRequest { amount, fee_per_gram: fee, message: "Burning some tari".to_string(), + claim_public_key: identity.public_key, }; let result = client.create_burn_transaction(req).await.unwrap();