From 610f5e157dec51ea66579788ddea2137bd3f25ca Mon Sep 17 00:00:00 2001 From: sword_smith Date: Wed, 9 Oct 2024 17:41:40 +0200 Subject: [PATCH] WIP: continue work on merge --- src/bin/dashboard_src/send_screen.rs | 2 +- src/bin/neptune-cli.rs | 2 +- src/main_loop.rs | 3 +- src/mine_loop.rs | 4 +- src/models/blockchain/transaction/mod.rs | 2 +- .../transaction/transaction_output.rs | 297 ++++++------- .../blockchain/type_scripts/neptune_coins.rs | 2 +- src/models/channel.rs | 4 +- src/models/state/mempool.rs | 2 - src/models/state/mod.rs | 406 ++++++------------ src/models/state/transaction_details.rs | 117 +++++ .../state/wallet/address/address_type.rs | 19 +- .../wallet/address/generation_address.rs | 21 +- .../state/wallet/address/symmetric_key.rs | 10 +- src/models/state/wallet/expected_utxo.rs | 17 +- src/models/state/wallet/mod.rs | 2 +- src/models/state/wallet/wallet_state.rs | 9 +- src/peer_loop.rs | 60 +-- src/rpc_server.rs | 49 ++- src/tests/shared.rs | 6 +- 20 files changed, 507 insertions(+), 527 deletions(-) create mode 100644 src/models/state/transaction_details.rs diff --git a/src/bin/dashboard_src/send_screen.rs b/src/bin/dashboard_src/send_screen.rs index 87d5038f4..84268190c 100644 --- a/src/bin/dashboard_src/send_screen.rs +++ b/src/bin/dashboard_src/send_screen.rs @@ -8,7 +8,7 @@ use crossterm::event::Event; use crossterm::event::KeyCode; use crossterm::event::KeyEventKind; use neptune_core::config_models::network::Network; -use neptune_core::models::blockchain::transaction::UtxoNotifyMethod; +use neptune_core::models::blockchain::transaction::transaction_output::UtxoNotifyMethod; use neptune_core::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; use neptune_core::models::state::wallet::address::ReceivingAddress; use neptune_core::rpc_server::RPCClient; diff --git a/src/bin/neptune-cli.rs b/src/bin/neptune-cli.rs index b84dea9af..924f0501e 100644 --- a/src/bin/neptune-cli.rs +++ b/src/bin/neptune-cli.rs @@ -14,7 +14,7 @@ use clap_complete::Shell; use neptune_core::config_models::data_directory::DataDirectory; use neptune_core::config_models::network::Network; use neptune_core::models::blockchain::block::block_selector::BlockSelector; -use neptune_core::models::blockchain::transaction::UtxoNotifyMethod; +use neptune_core::models::blockchain::transaction::transaction_output::UtxoNotifyMethod; use neptune_core::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; use neptune_core::models::state::wallet::address::KeyType; use neptune_core::models::state::wallet::address::ReceivingAddress; diff --git a/src/main_loop.rs b/src/main_loop.rs index 050c9e99c..8d746c0af 100644 --- a/src/main_loop.rs +++ b/src/main_loop.rs @@ -37,6 +37,7 @@ use crate::models::peer::HandshakeData; use crate::models::peer::PeerInfo; use crate::models::peer::PeerSynchronizationState; use crate::models::peer::TransactionNotification; +use crate::models::state::wallet::expected_utxo; use crate::models::state::GlobalStateLock; use crate::prelude::twenty_first; @@ -1011,7 +1012,7 @@ impl MainLoopHandler { /// after handling this message. async fn handle_rpc_server_message(&mut self, msg: RPCServerToMain) -> Result { match msg { - RPCServerToMain::Send(transaction) => { + RPCServerToMain::BroadcastTx(transaction) => { debug!( "`main` received following transaction from RPC Server. {} inputs, {} outputs. Synced to mutator set hash: {}", transaction.kernel.inputs.len(), diff --git a/src/mine_loop.rs b/src/mine_loop.rs index 7c0553a8a..81f75ef86 100644 --- a/src/mine_loop.rs +++ b/src/mine_loop.rs @@ -291,7 +291,7 @@ pub(crate) fn make_coinbase_transaction( vec![coinbase_utxo.clone()], vec![sender_randomness], vec![receiver_digest], - &kernel, + kernel, mutator_set_accumulator, ); @@ -310,7 +310,7 @@ pub(crate) fn make_coinbase_transaction( ( Transaction { - kernel, + kernel: primitive_witness.kernel, proof: TransactionProof::SingleProof(proof), }, utxo_info_for_coinbase, diff --git a/src/models/blockchain/transaction/mod.rs b/src/models/blockchain/transaction/mod.rs index 6cdb5b8d8..b7831f24f 100644 --- a/src/models/blockchain/transaction/mod.rs +++ b/src/models/blockchain/transaction/mod.rs @@ -8,7 +8,7 @@ use crate::prelude::twenty_first; pub mod lock_script; pub mod primitive_witness; pub mod transaction_kernel; -pub(crate) mod transaction_output; +pub mod transaction_output; pub mod utxo; pub mod validity; diff --git a/src/models/blockchain/transaction/transaction_output.rs b/src/models/blockchain/transaction/transaction_output.rs index d7197a72e..0c234fbaa 100644 --- a/src/models/blockchain/transaction/transaction_output.rs +++ b/src/models/blockchain/transaction/transaction_output.rs @@ -9,21 +9,19 @@ use serde::Serialize; use crate::models::blockchain::shared::Hash; use crate::models::blockchain::transaction::utxo::Utxo; -use crate::models::blockchain::transaction::PublicAnnouncement; use crate::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; use crate::models::state::wallet::address::ReceivingAddress; use crate::models::state::wallet::address::SpendingKey; -use crate::models::state::wallet::expected_utxo::ExpectedUtxo; use crate::models::state::wallet::wallet_state::WalletState; use crate::prelude::twenty_first::math::digest::Digest; use crate::prelude::twenty_first::util_types::algebraic_hasher::AlgebraicHasher; use crate::util_types::mutator_set::addition_record::AdditionRecord; use crate::util_types::mutator_set::commit; -/// enumerates how utxos should be transferred. +/// enumerates how utxos and spending information is communicated. /// /// see also: [UtxoNotification] -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum UtxoNotifyMethod { /// the utxo notification should be transferred to recipient encrypted on the blockchain OnChain, @@ -32,33 +30,17 @@ pub enum UtxoNotifyMethod { OffChain, } -/// enumerates utxo transfer methods with payloads -/// -/// [PublicAnnouncement] is essentially opaque however one can determine the key -/// type via [`KeyType::try_from::()`](crate::models::state::wallet::address::KeyType::try_from::()) -/// -/// see also: [UtxoNotifyMethod], [KeyType](crate::models::state::wallet::address::KeyType) +/// The payload of a UTXO notification, containing all information necessary +/// to claim it, provided access to the associated spending key. /// /// future work: -/// -/// we should consider adding this variant that would facilitate passing -/// utxo from sender to receiver off-chain for lower-fee transfers between -/// trusted parties or eg wallets owned by the same person/org. -/// -/// OffChainSerialized(PublicAnnouncement) -/// -/// also, perhaps PublicAnnouncement should be used for `OffChain` -/// and replace ExpectedUtxo. to consolidate code/logic. -/// -/// see comment for: [TxOutput::auto()] -/// +/// we should consider adding functionality that would facilitate passing +/// these payloads from sender to receiver off-chain for lower-fee transfers +/// between trusted parties or eg wallets owned by the same person/org. #[derive(Debug, Clone)] -pub enum UtxoNotification { - /// the utxo notification should be transferred to recipient on the blockchain as a [PublicAnnouncement] - OnChain(PublicAnnouncement), - - /// the utxo notification should be transferred to recipient off the blockchain as an [ExpectedUtxo] - OffChain(Box), +pub struct UtxoNotificationPayload { + pub utxo: Utxo, + pub sender_randomness: Digest, } /// represents a transaction output, as accepted by @@ -68,30 +50,18 @@ pub enum UtxoNotification { /// and claim a given UTXO #[derive(Debug, Clone)] pub struct TxOutput { - pub utxo: Utxo, - pub sender_randomness: Digest, - pub receiver_privacy_digest: Digest, - pub utxo_notification: UtxoNotification, -} - -impl From for TxOutput { - fn from(expected_utxo: ExpectedUtxo) -> Self { - Self { - utxo: expected_utxo.utxo.clone(), - sender_randomness: expected_utxo.sender_randomness, - receiver_privacy_digest: expected_utxo.receiver_preimage.hash(), - utxo_notification: UtxoNotification::OffChain(Box::new(expected_utxo)), - } - } + pub notification_payload: UtxoNotificationPayload, + pub notification_method: UtxoNotifyMethod, + pub receiving_address: ReceivingAddress, } impl From<&TxOutput> for AdditionRecord { /// retrieves public announcements from possible sub-set of the list - fn from(ur: &TxOutput) -> Self { + fn from(txo: &TxOutput) -> Self { commit( - Hash::hash(&ur.utxo), - ur.sender_randomness, - ur.receiver_privacy_digest, + Hash::hash(&txo.notification_payload.utxo), + txo.notification_payload.sender_randomness, + txo.receiving_address.privacy_digest(), ) } } @@ -103,7 +73,7 @@ impl TxOutput { /// will be used. A [PublicAnnouncement] will be created using whichever /// address type is provided. /// - /// If the [Utxo] can be claimed by our wallet, then + /// If the [Utxo] can be claimed by our wallet, then parameter /// `owned_utxo_notify_method` dictates the behavior: /// /// * `OffChain` results in local state transfer via whichever address type is provided. @@ -146,31 +116,26 @@ impl TxOutput { sender_randomness: Digest, owned_utxo_notify_method: UtxoNotifyMethod, ) -> Result { - let onchain = || -> Result { + let onchain = || -> TxOutput { let utxo = Utxo::new_native_coin(address.lock_script(), amount); - let pub_ann = address.generate_public_announcement(&utxo, sender_randomness)?; - Ok(Self::onchain( - utxo, - sender_randomness, - address.privacy_digest(), - pub_ann, - )) + Self::onchain(utxo, sender_randomness, address.to_owned()) }; - let offchain = |key: SpendingKey| { + let offchain = || { let utxo = Utxo::new_native_coin(address.lock_script(), amount); - Self::offchain(utxo, sender_randomness, key.privacy_preimage()) + Self::offchain(utxo, sender_randomness, address.to_owned()) }; let utxo = Utxo::new_native_coin(address.lock_script(), amount); - let utxo_wallet_key = wallet_state.find_spending_key_for_utxo(&utxo); - - let tx_output = match utxo_wallet_key { - None => onchain()?, - Some(key) => match owned_utxo_notify_method { - UtxoNotifyMethod::OnChain => onchain()?, - UtxoNotifyMethod::OffChain => offchain(key), - }, + let has_matching_spending_key = wallet_state.can_unlock(&utxo); + + let tx_output = if has_matching_spending_key { + match owned_utxo_notify_method { + UtxoNotifyMethod::OnChain => onchain(), + UtxoNotifyMethod::OffChain => offchain(), + } + } else { + onchain() }; Ok(tx_output) @@ -182,14 +147,16 @@ impl TxOutput { pub fn onchain( utxo: Utxo, sender_randomness: Digest, - receiver_privacy_digest: Digest, - public_announcement: PublicAnnouncement, + receiving_address: ReceivingAddress, ) -> Self { - Self { + let payload = UtxoNotificationPayload { utxo, sender_randomness, - receiver_privacy_digest, - utxo_notification: UtxoNotification::OnChain(public_announcement), + }; + Self { + notification_payload: payload, + notification_method: UtxoNotifyMethod::OnChain, + receiving_address, } } @@ -199,49 +166,33 @@ impl TxOutput { pub fn offchain( utxo: Utxo, sender_randomness: Digest, - receiver_privacy_digest: Digest, + receiving_address: ReceivingAddress, ) -> Self { - Self { + let payload = UtxoNotificationPayload { utxo, sender_randomness, - receiver_privacy_digest, - utxo_notification: UtxoNotification::OffChain(Box::new( - // Remark: UtxoNotification cannot contain an ExpectedUtxo because - // the second structure contains secret information known only to - // the receiver. UtxoNotification is created by the sender, who - // oblivious to these secrets. - todo!(), - )), + }; + Self { + notification_payload: payload, + notification_method: UtxoNotifyMethod::OffChain, + receiving_address, } } - // only for legacy tests - #[cfg(test)] - pub fn fake_address( - utxo: Utxo, - sender_randomness: Digest, - receiver_privacy_digest: Digest, - ) -> Self { - use crate::models::state::wallet::address::generation_address::GenerationReceivingAddress; + pub(crate) fn is_offchain(&self) -> bool { + matches!(self.notification_method, UtxoNotifyMethod::OffChain) + } - let address: ReceivingAddress = - GenerationReceivingAddress::derive_from_seed(rand::random()).into(); - let announcement = address - .generate_public_announcement(&utxo, sender_randomness) - .unwrap(); + pub(crate) fn utxo(&self) -> Utxo { + self.notification_payload.utxo.clone() + } - Self { - utxo, - sender_randomness, - receiver_privacy_digest, - utxo_notification: UtxoNotification::OnChain(announcement), - } + pub(crate) fn sender_randomness(&self) -> Digest { + self.notification_payload.sender_randomness } - // only for legacy tests - #[cfg(test)] - pub fn random(utxo: Utxo) -> Self { - Self::fake_address(utxo, rand::random(), rand::random()) + pub(crate) fn receiver_digest(&self) -> Digest { + self.receiving_address.privacy_digest() } } @@ -281,30 +232,32 @@ impl From<&TxOutputList> for Vec { } } -impl From<&TxOutputList> for Vec { - fn from(list: &TxOutputList) -> Self { - list.expected_utxos_iter().collect() - } -} +// Killed because: this mapping requires wallet info! +// impl From<&TxOutputList> for Vec { +// fn from(list: &TxOutputList) -> Self { +// list.expected_utxos_iter().collect() +// } +// } -impl From<&TxOutputList> for Vec { - fn from(list: &TxOutputList) -> Self { - list.public_announcements_iter().into_iter().collect() - } -} +// Killed because: this mapping requires recipient info! +// impl From<&TxOutputList> for Vec { +// fn from(list: &TxOutputList) -> Self { +// list.public_announcements_iter().into_iter().collect() +// } +// } impl TxOutputList { /// calculates total amount in native currency pub fn total_native_coins(&self) -> NeptuneCoins { self.0 .iter() - .map(|u| u.utxo.get_native_currency_amount()) + .map(|u| u.notification_payload.utxo.get_native_currency_amount()) .sum() } /// retrieves utxos pub fn utxos_iter(&self) -> impl IntoIterator + '_ { - self.0.iter().map(|u| u.utxo.clone()) + self.0.iter().map(|u| u.notification_payload.utxo.clone()) } /// retrieves utxos @@ -323,36 +276,50 @@ impl TxOutputList { } /// retrieves public announcements from possible sub-set of the list - pub fn public_announcements_iter(&self) -> impl IntoIterator + '_ { - self.0.iter().filter_map(|u| match &u.utxo_notification { - UtxoNotification::OnChain(pa) => Some(pa.clone()), - _ => None, - }) - } + /// + /// Do we really want this function? We want to ensure that we can claim *our* + /// output UTXOs; but that is already achieved through `UtxoNotification`. + /// `PublicAnnouncement`s can be `UtxoNotification`s but not necessarily, and + /// when they are they announce UTXOs that are generally intended for others. + /// + // Killed because: going from `TxOutput` to `PublicAnnouncement` requires + // either wallet info or recipient info; but neither are part of the signature. + // pub fn public_announcements_iter(&self) -> impl IntoIterator + '_ { + // self.0.iter().filter_map(|u| match &u.utxo_notification { + // UtxoNotification::OnChain(pa) => Some(pa.clone()), + // _ => None, + // }) + // } /// retrieves public announcements from possible sub-set of the list - pub fn public_announcements(&self) -> Vec { - self.public_announcements_iter().into_iter().collect() - } + // Killed because: see `public_announcements_iter`. + // pub(crate) fn public_announcements(&self) -> Vec { + // self.public_announcements_iter().into_iter().collect() + // } /// retrieves expected_utxos from possible sub-set of the list - pub fn expected_utxos_iter(&self) -> impl Iterator + '_ { - self.0.iter().filter_map(|u| match &u.utxo_notification { - UtxoNotification::OffChain(eu) => Some(*eu.clone()), - _ => None, - }) - } + // Killed because: going from `TxOutput` to `ExpectedUtxo` requires wallet + // info. + // pub fn expected_utxos_iter(&self) -> impl Iterator + '_ { + // self.0.iter().filter_map(|u| match &u.utxo_notification { + // UtxoNotification::OffChain(eu) => Some(*eu.clone()), + // _ => None, + // }) + // } - /// indicates if any offchain notifications (ExpectedUtxo) exist + /// retrieves expected_utxos from possible sub-set of the list + // Killed because: see `expected_utxos_iter` + // pub fn expected_utxos(&self) -> Vec { + // self.expected_utxos_iter().collect() + // } + + /// indicates if any offchain notifications exist pub fn has_offchain(&self) -> bool { - self.0 - .iter() - .any(|u| matches!(&u.utxo_notification, UtxoNotification::OffChain(_))) + self.0.iter().any(|u| u.is_offchain()) } - /// retrieves expected_utxos from possible sub-set of the list - pub fn expected_utxos(&self) -> Vec { - self.expected_utxos_iter().collect() + pub fn push(&mut self, tx_output: TxOutput) { + self.0.push(tx_output); } } @@ -390,7 +357,7 @@ mod tests { .generate_sender_randomness(block_height, address.privacy_digest); for utxo_notify_method in [UtxoNotifyMethod::OffChain, UtxoNotifyMethod::OnChain] { - let utxo_receiver = TxOutput::auto( + let tx_output = TxOutput::auto( &state.wallet_state, &address.into(), amount, @@ -400,16 +367,14 @@ mod tests { // we should have OnChain transfer regardless of owned_transfer_method setting // because it only applies to owned outputs. - assert!(matches!( - utxo_receiver.utxo_notification, - UtxoNotification::OnChain(_) - )); - assert_eq!(utxo_receiver.sender_randomness, sender_randomness); - assert_eq!( - utxo_receiver.receiver_privacy_digest, - address.privacy_digest - ); - assert_eq!(utxo_receiver.utxo, utxo); + // assert!(matches!( + // tx_output.utxo_notification, + // ::OnChain(_) + // )); + assert_eq!(utxo_notify_method, tx_output.notification_method); + assert_eq!(tx_output.sender_randomness(), sender_randomness); + assert_eq!(tx_output.receiver_digest(), address.privacy_digest); + assert_eq!(tx_output.utxo(), utxo); } Ok(()) @@ -451,34 +416,30 @@ mod tests { .wallet_secret .generate_sender_randomness(block_height, address.privacy_digest()); - let utxo_receiver = TxOutput::auto( + let tx_output = TxOutput::auto( &state.wallet_state, &address, amount, sender_randomness, - transfer_method, // how to notify of owned utxos. + transfer_method, // how to notify of utxos sent to myself )?; - let transfer_is_correct = match utxo_receiver.utxo_notification { - UtxoNotification::OffChain(_) => { - matches!(transfer_method, UtxoNotifyMethod::OffChain) - } - UtxoNotification::OnChain(ref pa) => match transfer_method { - UtxoNotifyMethod::OnChain => address.matches_public_announcement_key_type(pa), - _ => false, - }, - }; + assert_eq!(transfer_method, tx_output.notification_method); + assert_eq!( + sender_randomness, + tx_output.notification_payload.sender_randomness + ); + assert_eq!( + address.lock_script().hash(), + tx_output.utxo().lock_script_hash + ); println!("owned_transfer_method: {:#?}", transfer_method); - println!("utxo_transfer: {:#?}", utxo_receiver.utxo_notification); + println!("utxo_transfer: {:#?}", tx_output.notification_payload); - assert!(transfer_is_correct); - assert_eq!(utxo_receiver.sender_randomness, sender_randomness); - assert_eq!( - utxo_receiver.receiver_privacy_digest, - address.privacy_digest() - ); - assert_eq!(utxo_receiver.utxo, utxo); + assert_eq!(tx_output.sender_randomness(), sender_randomness); + assert_eq!(tx_output.receiver_digest(), address.privacy_digest()); + assert_eq!(tx_output.utxo(), utxo); } Ok(()) diff --git a/src/models/blockchain/type_scripts/neptune_coins.rs b/src/models/blockchain/type_scripts/neptune_coins.rs index dfb00ba31..be5f5e355 100644 --- a/src/models/blockchain/type_scripts/neptune_coins.rs +++ b/src/models/blockchain/type_scripts/neptune_coins.rs @@ -44,7 +44,7 @@ use crate::models::proof_abstractions::tasm::program::ConsensusProgram; /// program related to block validity, it is important to use `safe_add` rather than `+` as /// the latter operation does not care about overflow. Not testing for overflow can cause /// inflation bugs. -#[derive(Clone, Copy, Serialize, Deserialize, Eq, BFieldCodec, TasmObject)] +#[derive(Clone, Copy, Serialize, Deserialize, Eq, BFieldCodec, TasmObject, Default)] pub struct NeptuneCoins(u128); impl NeptuneCoins { diff --git a/src/models/channel.rs b/src/models/channel.rs index 255d022de..3624e53d7 100644 --- a/src/models/channel.rs +++ b/src/models/channel.rs @@ -100,7 +100,7 @@ impl PeerTaskToMain { #[derive(Clone, Debug)] pub enum RPCServerToMain { - Send(Box), + BroadcastTx(Box), Shutdown, PauseMiner, RestartMiner, @@ -109,7 +109,7 @@ pub enum RPCServerToMain { impl RPCServerToMain { pub fn get_type(&self) -> String { match self { - RPCServerToMain::Send(_) => "initiate transaction".to_string(), + RPCServerToMain::BroadcastTx(_) => "broadcast transaction".to_string(), RPCServerToMain::Shutdown => "shutdown".to_string(), RPCServerToMain::PauseMiner => "pause miner".to_owned(), RPCServerToMain::RestartMiner => "restart miner".to_owned(), diff --git a/src/models/state/mempool.rs b/src/models/state/mempool.rs index 761ed6732..1b00403ff 100644 --- a/src/models/state/mempool.rs +++ b/src/models/state/mempool.rs @@ -457,9 +457,7 @@ mod tests { use itertools::Itertools; use num_bigint::BigInt; use num_traits::Zero; - use rand::random; use rand::rngs::StdRng; - use rand::thread_rng; use rand::Rng; use rand::SeedableRng; use tracing::debug; diff --git a/src/models/state/mod.rs b/src/models/state/mod.rs index 56ec1296e..cd09a7566 100644 --- a/src/models/state/mod.rs +++ b/src/models/state/mod.rs @@ -4,6 +4,7 @@ pub mod light_state; pub mod mempool; pub mod networking_state; pub mod shared; +pub(crate) mod transaction_details; pub mod tx_proving_capability; pub mod wallet; @@ -25,6 +26,7 @@ use tasm_lib::triton_vm::prelude::*; use tracing::debug; use tracing::info; use tracing::warn; +use transaction_details::TransactionDetails; use twenty_first::math::digest::Digest; use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; use tx_proving_capability::TxProvingCapability; @@ -42,6 +44,7 @@ use super::blockchain::transaction::primitive_witness::SaltedUtxos; use super::blockchain::transaction::transaction_kernel::TransactionKernel; use super::blockchain::transaction::transaction_output::TxOutput; use super::blockchain::transaction::transaction_output::TxOutputList; +use super::blockchain::transaction::transaction_output::UtxoNotificationPayload; use super::blockchain::transaction::transaction_output::UtxoNotifyMethod; use super::blockchain::transaction::utxo::Utxo; use super::blockchain::transaction::Transaction; @@ -73,14 +76,6 @@ use crate::util_types::mutator_set::removal_record::RemovalRecord; use crate::Hash; use crate::VERSION; -#[derive(Debug, Clone)] -struct TransactionDetails { - pub tx_inputs: Vec, - pub tx_outputs: TxOutputList, - pub fee: NeptuneCoins, - pub timestamp: Timestamp, -} - /// `GlobalStateLock` holds a [`tokio::AtomicRw`](crate::locks::tokio::AtomicRw) /// ([`RwLock`](std::sync::RwLock)) over [`GlobalState`]. /// @@ -411,26 +406,6 @@ impl GlobalState { history } - /// Given the desired outputs, assemble UTXOs that are both spendable - /// (*i.e.*, synced and never or no longer timelocked) and that sum to - /// enough funds. - pub(crate) async fn assemble_inputs_for_transaction( - &mut self, - total_spend: NeptuneCoins, - timestamp: Timestamp, - ) -> Result> { - // Get the block tip as the transaction is made relative to it - let block_tip = self.chain.light_state(); - - // collect spendable inputs - let spendable_utxos_and_mps = self - .wallet_state - .allocate_sufficient_input_funds(total_spend, block_tip.hash(), timestamp) - .await?; - - Ok(spendable_utxos_and_mps) - } - /// Given a list of spendable UTXOs, generate the corresponding removal /// recods relative to the current mutator set accumulator. pub(crate) fn generate_removal_records( @@ -462,15 +437,15 @@ impl GlobalState { } /// Generate a change UTXO to ensure that the difference in input amount - /// and output amount goes back to us. Also, add the change UTXO to the - /// list of expected UTXOs. - pub async fn create_change_utxo(&mut self, change_amount: NeptuneCoins) -> TxOutput { - // generate utxo - let own_spending_key_for_change = self - .wallet_state - .wallet_secret - .nth_generation_spending_key(0); - let own_receiving_address = own_spending_key_for_change.to_address(); + /// and output amount goes back to us. Return the UTXO in a format compatible + /// with claiming it later on, *i.e.*, as an [ExpectedUtxo]. + pub fn create_change_output( + &self, + change_amount: NeptuneCoins, + change_key: SpendingKey, + change_utxo_notify_method: UtxoNotifyMethod, + ) -> Result { + let own_receiving_address = change_key.to_address(); let lock_script = own_receiving_address.lock_script(); let lock_script_hash = lock_script.hash(); let change_utxo = Utxo { @@ -478,29 +453,27 @@ impl GlobalState { lock_script_hash, }; - let receiver_digest = own_receiving_address.privacy_digest; + let receiver_digest = own_receiving_address.privacy_digest(); let change_sender_randomness = self.wallet_state.wallet_secret.generate_sender_randomness( self.chain.light_state().kernel.header.height, receiver_digest, ); // Add change UTXO to pool of expected incoming UTXOs - let receiver_preimage = own_spending_key_for_change.privacy_preimage; - let _change_addition_record = self - .wallet_state - .add_expected_utxo(ExpectedUtxo::new( - change_utxo.clone(), + let change_output = match change_utxo_notify_method { + UtxoNotifyMethod::OnChain => TxOutput::onchain( + change_utxo, change_sender_randomness, - receiver_preimage, - UtxoNotifier::Myself, - )) - .await; + change_key.to_address().privacy_digest(), + ), + UtxoNotifyMethod::OffChain => TxOutput::offchain( + change_utxo, + change_sender_randomness, + change_key.to_address().privacy_digest(), + ), + }; - TxOutput::offchain( - change_utxo, - change_sender_randomness, - receiver_preimage.hash(), - ) + Ok(change_output) } /// Generate a primitive witness for a transaction from various disparate witness data. @@ -646,16 +619,16 @@ impl GlobalState { /// [Self::generate_tx_outputs()] which determines which outputs should be /// `OnChain` or `OffChain`. /// - /// After this call returns it is the caller's responsibility to inform the + /// The return value is the created transaction and some change UTXO with + /// associated data or none if the transaction is already balanced. The + /// associated data allows the caller to expect and later claim the change + /// UTXO. + /// + /// After this call returns, it is the caller's responsibility to inform the /// wallet of any returned [ExpectedUtxo], ie `OffChain` secret /// notifications, for utxos that match wallet keys. Failure to do so can /// result in loss of funds! /// - /// This function will modify the `tx_outputs` parameter by - /// appending an element representing the change output, if change is - /// needed. Any [ExpectedUtxo], including change can then be retrieved - /// with [TxOutputList::expected_utxos()]. - /// /// The `change_utxo_notify_method` parameter should normally be /// [UtxoNotifyMethod::OnChain] for safest transfer. /// @@ -670,7 +643,7 @@ impl GlobalState { /// /// ```compile_fail /// - /// // we obtain a change key first, as it requires modifying wallet state. + /// // obtain a change key /// // note that this is a SymmetricKey, not a regular (Generation) address. /// let change_key = global_state_lock /// .lock_guard_mut() @@ -679,23 +652,23 @@ impl GlobalState { /// .wallet_secret /// .next_unused_spending_key(KeyType::Symmetric); /// - /// // we choose onchain notification for all utxos destined for our wallet. - /// let notify_method = UtxoNotifyMethod::OnChain; + /// // on-chain notification for all utxos destined for our wallet. + /// let change_notify_method = UtxoNotifyMethod::OnChain; /// /// // obtain read lock /// let state = self.state.lock_guard().await; /// /// // generate the tx_outputs - /// let mut tx_outputs = state.generate_tx_outputs(outputs, notify_method)?; + /// let mut tx_outputs = state.generate_tx_outputs(outputs, change_notify_method)?; /// /// // Create the transaction - /// let transaction = state + /// let (transaction, maybe_change_utxo) = state /// .create_transaction( - /// &mut tx_outputs, // all outputs except `change` + /// tx_outputs, // all outputs except `change` /// change_key, // send `change` to this key - /// notify_method, // how to notify about `change` utxo + /// change_notify_method, // how to notify about `change` utxo /// NeptuneCoins::new(2), // fee - /// Timestamp::now(), + /// Timestamp::now(), // Timestamp of transaction /// ) /// .await?; /// @@ -703,20 +676,22 @@ impl GlobalState { /// drop(state); /// /// // Inform wallet of any expected incoming utxos. - /// state - /// .lock_guard_mut() - /// .await - /// .add_expected_utxos_to_wallet(tx_outputs.expected_utxos()) - /// .await?; + /// if let Some(change_utxo) = maybe_change_utxo { + /// state + /// .lock_guard_mut() + /// .await + /// .add_expected_utxos_to_wallet(change_utxo.expected_utxo()) + /// .await?; + /// } /// ``` pub async fn create_transaction( &self, - tx_outputs: &mut TxOutputList, + tx_outputs: TxOutputList, change_key: SpendingKey, change_utxo_notify_method: UtxoNotifyMethod, fee: NeptuneCoins, timestamp: Timestamp, - ) -> Result { + ) -> Result<(Transaction, Option)> { self.create_transaction_with_prover_capability( tx_outputs, change_key, @@ -733,76 +708,57 @@ impl GlobalState { /// for anything but tests. pub(crate) async fn create_transaction_with_prover_capability( &self, - tx_outputs: &mut TxOutputList, + mut tx_outputs: TxOutputList, change_key: SpendingKey, change_utxo_notify_method: UtxoNotifyMethod, fee: NeptuneCoins, timestamp: Timestamp, prover_capability: TxProvingCapability, - ) -> Result { + ) -> Result<(Transaction, Option)> { + let tip = self.chain.light_state(); + let tip_mutator_set_accumulator = tip.kernel.body.mutator_set_accumulator.clone(); + let tip_digest = tip.hash(); + // 1. create/add change output if necessary. let total_spend = tx_outputs.total_native_coins() + fee; - let tip_hash = self.chain.light_state().hash(); - // collect spendable inputs let tx_inputs = self .wallet_state - .allocate_sufficient_input_funds(total_spend, tip_hash, timestamp) + .allocate_sufficient_input_funds(total_spend, tip_digest, timestamp) .await?; - let input_amount = tx_inputs + let total_spendable = tx_inputs .iter() .map(|x| x.utxo.get_native_currency_amount()) .sum(); - if total_spend < input_amount { - let block_height = self.chain.light_state().header().height; - - let amount = input_amount.checked_sub(&total_spend).ok_or_else(|| { - anyhow::anyhow!("underflow subtracting total_spend from input_amount") + // Add change, if required to balance tx. + let mut maybe_change_output = None; + if total_spend < total_spendable { + let amount = total_spendable.checked_sub(&total_spend).ok_or_else(|| { + anyhow::anyhow!("overflow subtracting total_spend from input_amount") })?; - let tx_output = { - let utxo = Utxo::new_native_coin(change_key.to_address().lock_script(), amount); - let sender_randomness = self.wallet_state.wallet_secret.generate_sender_randomness( - block_height, - change_key.to_address().privacy_digest(), - ); - - match change_utxo_notify_method { - UtxoNotifyMethod::OnChain => { - let public_announcement = change_key - .to_address() - .generate_public_announcement(&utxo, sender_randomness)?; - TxOutput::onchain( - utxo, - sender_randomness, - change_key.to_address().privacy_digest(), - public_announcement, - ) - } - UtxoNotifyMethod::OffChain => { - TxOutput::offchain(utxo, sender_randomness, change_key.privacy_preimage()) - } - } - }; - - tx_outputs.push(tx_output); + let change_utxo = + self.create_change_output(amount, change_key, change_utxo_notify_method)?; + tx_outputs.push(change_utxo.clone()); + maybe_change_output = Some(change_utxo); } + let transaction_details = TransactionDetails::new_without_coinbase( + tx_inputs, + tx_outputs.to_owned(), + fee, + timestamp, + tip_mutator_set_accumulator, + )?; + // 2. Create the transaction - let transaction = self - .create_raw_transaction( - tx_inputs, - tx_outputs.clone(), - fee, - timestamp, - prover_capability, - ) - .await?; + let transaction = + Self::create_raw_transaction(transaction_details, prover_capability).await?; - Ok(transaction) + Ok((transaction, maybe_change_output)) } /// creates a Transaction. @@ -819,9 +775,9 @@ impl GlobalState { /// /// The `tx_outputs` parameter should normally be generated with /// [Self::generate_tx_outputs()] which determines which outputs should be - /// `OnChain` or `OffChain`. + /// notified `OnChain` or `OffChain`. /// - /// After this call returns it is the caller's responsibility to inform the + /// After this call returns, it is the caller's responsibility to inform the /// wallet of any returned [ExpectedUtxo] for utxos that match wallet keys. /// Failure to do so can result in loss of funds! /// @@ -833,187 +789,38 @@ impl GlobalState { /// /// See the implementation of [Self::create_transaction()]. pub(crate) async fn create_raw_transaction( - &self, - tx_inputs: Vec, - receiver_data: TxOutputList, - fee: NeptuneCoins, - timestamp: Timestamp, - prover_capability: TxProvingCapability, - ) -> Result { - // UTXO data: inputs, outputs, and supporting witness data - let tx_data = self - .generate_tx_details_for_transaction(tx_inputs, receiver_data, fee, timestamp) - .await?; - - self.create_transaction_from_data(tx_data, prover_capability) - .await - } - - /// This is a simple wrapper around create_transaction - /// for compatibility with existing tests. - #[cfg(test)] - pub async fn create_transaction_test_wrapper_kill_me( - &self, - tx_output_vec: Vec, - fee: NeptuneCoins, - timestamp: Timestamp, - ) -> Result<(Transaction, Vec)> { - let mut tx_outputs = TxOutputList::from(tx_output_vec); - - // note: should use next_unused_generation_spending_key() - // but that requires &mut self. - let change_key = self - .wallet_state - .wallet_secret - .nth_symmetric_key_for_tests(0); - - let len = tx_outputs.len(); - let transaction = self - .create_transaction( - &mut tx_outputs, - change_key.into(), - UtxoNotifyMethod::OffChain, - fee, - timestamp, - ) - .await?; - info!("receivers len before: {len}, after: {}", tx_outputs.len()); - Ok((transaction, (&tx_outputs).into())) - } - - /// Given a list of UTXOs with receiver data, assemble owned and synced and spendable - /// UTXOs that unlock enough funds, add (and track) a change UTXO if necessary, and - /// and produce a list of removal records, input UTXOs (with lock scripts and - /// membership proofs), addition records, and output UTXOs. - async fn generate_tx_details_for_transaction( - &self, - tx_inputs: Vec, - tx_outputs: TxOutputList, - fee: NeptuneCoins, - timestamp: Timestamp, - ) -> Result { - // total amount to be spent -- determines how many and which UTXOs to use - let total_spend: NeptuneCoins = tx_outputs.total_native_coins() + fee; - let input_amount = tx_inputs - .iter() - .map(|x| x.utxo.get_native_currency_amount()) - .sum(); - - // sanity check: do we even have enough funds? - if total_spend > input_amount { - debug!("Insufficient funds. total_spend: {total_spend}, input_amount: {input_amount}"); - bail!("Not enough available funds."); - } - if total_spend < input_amount { - let diff = total_spend - input_amount; - bail!("Missing change output in the amount of {}", diff); - } - - Ok(TransactionDetails { - tx_inputs, - tx_outputs, - fee, - timestamp, - }) - } - - /// Assembles a transaction kernel and supporting witness or proof(s) from - /// the given transaction data. - async fn create_transaction_from_data( - &self, transaction_details: TransactionDetails, proving_power: TxProvingCapability, ) -> Result { - let mutator_set_accumulator = self - .chain - .light_state() - .kernel - .body - .mutator_set_accumulator - .clone(); - // note: this executes the prover which can take a very // long time, perhaps minutes. As such, we use // spawn_blocking() to execute on tokio's blocking // threadpool and avoid blocking the tokio executor // and other async tasks. let transaction = tokio::task::spawn_blocking(move || { - Self::create_transaction_from_data_worker( - transaction_details, - mutator_set_accumulator, - proving_power, - ) + Self::create_transaction_from_data_worker(transaction_details, proving_power) }) .await?; Ok(transaction) } - /// Given a list of UTXOs with receiver data, assemble owned and synced and spendable - /// UTXOs that unlock enough funds, add (and track) a change UTXO if necessary, - /// and produce a list of removal records, input UTXOs (with lock scripts and - /// membership proofs), addition records, output UTXOs, their canonical commitments, - /// and the randmnesses used to produce them. - /// - /// Modifies the input receiver data to add a change-UTXO if needed. - async fn complete_transaction_details( - &mut self, - mut receiver_data: Vec, - fee: NeptuneCoins, - timestamp: Timestamp, - ) -> Result { - // total amount to be spent -- determines how many and which UTXOs to use - let total_spend: NeptuneCoins = receiver_data - .iter() - .map(|x| x.utxo.get_native_currency_amount()) - .sum::() - + fee; - - // collect enough spendable UTXOs - let unlockers: Vec = self - .assemble_inputs_for_transaction(total_spend, timestamp) - .await?; - let input_amount = unlockers - .iter() - .map(|unlocker| unlocker.utxo.get_native_currency_amount()) - .sum::(); - - // sanity check: do we even have enough funds? - if total_spend > input_amount { - bail!("Not enough available funds."); - } - - // Add change-UTXO to transaction - if total_spend < input_amount { - let change_amount = input_amount.checked_sub(&total_spend).unwrap(); - let change = self.create_change_utxo(change_amount).await; - receiver_data.push(change); - } - - Ok(TransactionDetails { - tx_inputs: unlockers, - tx_outputs: receiver_data.into(), - fee, - timestamp, - }) - } - // note: this executes the prover which can take a very // long time, perhaps minutes. It should never be // called directly. // Use create_transaction_from_data() instead. // - // fixme: why is _privacy param unused? fn create_transaction_from_data_worker( transaction_details: TransactionDetails, - mutator_set_accumulator: MutatorSetAccumulator, proving_power: TxProvingCapability, ) -> Transaction { let TransactionDetails { tx_inputs, tx_outputs, fee, + coinbase, timestamp, + mutator_set_accumulator, } = transaction_details; // complete transaction kernel @@ -1028,7 +835,7 @@ impl GlobalState { public_announcements: tx_outputs.public_announcements(), fee, timestamp, - coinbase: None, + coinbase, mutator_set_hash: mutator_set_accumulator.hash(), }; @@ -1675,6 +1482,37 @@ mod global_state_tests { use crate::tests::shared::mock_genesis_global_state; use crate::tests::shared::mock_genesis_wallet_state; + impl GlobalState { + pub(crate) async fn create_transaction_test_wrapper_kill_me( + &self, + tx_output_vec: Vec, + fee: NeptuneCoins, + timestamp: Timestamp, + ) -> Result<(Transaction, Vec)> { + let mut tx_outputs = TxOutputList::from(tx_output_vec); + + // note: should use next_unused_generation_spending_key() + // but that requires &mut self. + let change_key = self + .wallet_state + .wallet_secret + .nth_symmetric_key_for_tests(0); + + let len = tx_outputs.len(); + let transaction = self + .create_transaction( + &mut tx_outputs, + change_key.into(), + UtxoNotifyMethod::OffChain, + fee, + timestamp, + ) + .await?; + info!("receivers len before: {len}, after: {}", tx_outputs.len()); + Ok((transaction, (&tx_outputs).into())) + } + } + async fn wallet_state_has_all_valid_mps_for( wallet_state: &WalletState, tip_block: &Block, @@ -2246,7 +2084,7 @@ mod global_state_tests { let fee = NeptuneCoins::one(); let sender_randomness: Digest = rng.gen(); let tx_outputs_for_alice = vec![ - TxOutput::fake_address( + TxOutput::onchain( Utxo { lock_script_hash: alice_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(1).to_native_coins(), @@ -2254,7 +2092,7 @@ mod global_state_tests { sender_randomness, alice_spending_key.to_address().privacy_digest, ), - TxOutput::fake_address( + TxOutput::onchain( Utxo { lock_script_hash: alice_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(2).to_native_coins(), @@ -2266,7 +2104,7 @@ mod global_state_tests { // Two outputs for Bob let tx_outputs_for_bob = vec![ - TxOutput::fake_address( + TxOutput::onchain( Utxo { lock_script_hash: bob_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(3).to_native_coins(), @@ -2274,7 +2112,7 @@ mod global_state_tests { sender_randomness, bob_spending_key.to_address().privacy_digest, ), - TxOutput::fake_address( + TxOutput::onchain( Utxo { lock_script_hash: bob_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(4).to_native_coins(), @@ -2399,7 +2237,7 @@ mod global_state_tests { // Make two transactions: Alice sends two UTXOs to Genesis and Bob sends three UTXOs to genesis let tx_outputs_from_alice = vec![ - TxOutput::fake_address( + TxOutput::onchain( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(1).to_native_coins(), @@ -2407,7 +2245,7 @@ mod global_state_tests { rng.gen(), genesis_spending_key.to_address().privacy_digest, ), - TxOutput::fake_address( + TxOutput::onchain( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(1).to_native_coins(), @@ -2436,7 +2274,7 @@ mod global_state_tests { .unwrap(); let tx_outputs_from_bob = vec![ - TxOutput::fake_address( + TxOutput::onchain( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(2).to_native_coins(), @@ -2444,7 +2282,7 @@ mod global_state_tests { rng.gen(), genesis_spending_key.to_address().privacy_digest, ), - TxOutput::fake_address( + TxOutput::onchain( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(2).to_native_coins(), @@ -2452,7 +2290,7 @@ mod global_state_tests { rng.gen(), genesis_spending_key.to_address().privacy_digest, ), - TxOutput::fake_address( + TxOutput::onchain( Utxo { lock_script_hash: genesis_spending_key.to_address().lock_script().hash(), coins: NeptuneCoins::new(2).to_native_coins(), diff --git a/src/models/state/transaction_details.rs b/src/models/state/transaction_details.rs new file mode 100644 index 000000000..a90a40922 --- /dev/null +++ b/src/models/state/transaction_details.rs @@ -0,0 +1,117 @@ +use anyhow::bail; +use anyhow::Result; +use num_traits::Zero; +use tracing::debug; + +use super::wallet::unlocked_utxo::UnlockedUtxo; +use crate::models::blockchain::transaction::transaction_output::TxOutputList; +use crate::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; +use crate::models::proof_abstractions::timestamp::Timestamp; +use crate::util_types::mutator_set::mutator_set_accumulator::MutatorSetAccumulator; + +/// Information, fetched from the state of the node, required to generate a +/// transaction. +#[derive(Debug, Clone)] +pub(crate) struct TransactionDetails { + pub tx_inputs: Vec, + pub tx_outputs: TxOutputList, + pub fee: NeptuneCoins, + pub coinbase: Option, + pub timestamp: Timestamp, + pub mutator_set_accumulator: MutatorSetAccumulator, +} + +impl TransactionDetails { + /// Construct a [`TransactionDetails`] instance with coinbase from state + /// information. + /// + /// Does sanity checks on: + /// - amounts, must be balanced + /// - mutator set membership proofs, must be valid wrt. supplied mutator set + /// + /// See also: [Self::new_without_coinbase]. + pub(crate) fn new_with_coinbase( + tx_inputs: Vec, + tx_outputs: TxOutputList, + fee: NeptuneCoins, + coinbase: NeptuneCoins, + timestamp: Timestamp, + mutator_set_accumulator: MutatorSetAccumulator, + ) -> Result { + Self::new( + tx_inputs, + tx_outputs, + fee, + Some(coinbase), + timestamp, + mutator_set_accumulator, + ) + } + + /// Construct a [`TransactionDetails`] instance without coinbase from state + /// information. + /// + /// Does sanity checks on: + /// - amounts, must be balanced + /// - mutator set membership proofs, must be valid wrt. supplied mutator set + /// + /// See also: [Self::new_with_coinbase]. + pub(crate) fn new_without_coinbase( + tx_inputs: Vec, + tx_outputs: TxOutputList, + fee: NeptuneCoins, + timestamp: Timestamp, + mutator_set_accumulator: MutatorSetAccumulator, + ) -> Result { + Self::new( + tx_inputs, + tx_outputs, + fee, + None, + timestamp, + mutator_set_accumulator, + ) + } + + fn new( + tx_inputs: Vec, + tx_outputs: TxOutputList, + fee: NeptuneCoins, + coinbase: Option, + timestamp: Timestamp, + mutator_set_accumulator: MutatorSetAccumulator, + ) -> Result { + // total amount to be spent -- determines how many and which UTXOs to use + let total_spent = tx_outputs.total_native_coins() + fee; + let total_input: NeptuneCoins = tx_inputs + .iter() + .map(|x| x.utxo.get_native_currency_amount()) + .sum(); + let total_spendable = total_input + coinbase.unwrap_or(NeptuneCoins::zero()); + + // sanity check: do we even have enough funds? + if total_spent > total_spendable { + debug!("Insufficient funds. total_spend: {total_spent}, total_spendable: {total_spendable}"); + bail!("Not enough available funds."); + } + if total_spent < total_spendable { + let diff = total_spent - total_spendable; + bail!("Missing change output in the amount of {}", diff); + } + if tx_inputs + .iter() + .any(|x| !mutator_set_accumulator.verify(x.mutator_set_item(), x.mutator_set_mp())) + { + bail!("Invalid mutator set membership proof/mutator set pair provided."); + } + + Ok(TransactionDetails { + tx_inputs, + tx_outputs, + fee, + coinbase, + timestamp, + mutator_set_accumulator, + }) + } +} diff --git a/src/models/state/wallet/address/address_type.rs b/src/models/state/wallet/address/address_type.rs index df4392e10..b9c9b4bf9 100644 --- a/src/models/state/wallet/address/address_type.rs +++ b/src/models/state/wallet/address/address_type.rs @@ -10,12 +10,14 @@ use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; use super::common; use super::generation_address; +use super::generation_address::GenerationSpendingKey; use super::symmetric_key; use crate::config_models::network::Network; use crate::models::blockchain::shared::Hash; use crate::models::blockchain::transaction::lock_script::LockScript; use crate::models::blockchain::transaction::lock_script::LockScriptAndWitness; use crate::models::blockchain::transaction::transaction_kernel::TransactionKernel; +use crate::models::blockchain::transaction::transaction_output::UtxoNotificationPayload; use crate::models::blockchain::transaction::utxo::Utxo; use crate::models::blockchain::transaction::AnnouncedUtxo; use crate::models::blockchain::transaction::PublicAnnouncement; @@ -146,8 +148,8 @@ impl ReceivingAddress { /// generates a [PublicAnnouncement] for an output Utxo /// - /// The public announcement contains a Vec type flag. (SYMMETRIC_KEY_FLAG) + /// The public announcement contains a Vec with fields: + /// 0 --> type flag. (flag of key type) /// 1 --> receiver_identifier (fingerprint derived from seed) /// 2..n --> ciphertext (encrypted utxo + sender_randomness) /// @@ -155,8 +157,7 @@ impl ReceivingAddress { /// is intended for them and decryption should be attempted. pub fn generate_public_announcement( &self, - utxo: &Utxo, - sender_randomness: Digest, + utxo_notification_payload: UtxoNotificationPayload, ) -> Result { // let ciphertext = [ // &[KeyType::from(self).into(), self.receiver_identifier()], @@ -167,10 +168,10 @@ impl ReceivingAddress { // Ok(PublicAnnouncement::new(ciphertext)) match self { ReceivingAddress::Generation(generation_receiving_address) => { - generation_receiving_address.generate_public_announcement(utxo, sender_randomness) + generation_receiving_address.generate_public_announcement(utxo_notification_payload) } ReceivingAddress::Symmetric(symmetric_key) => { - symmetric_key.generate_public_announcement(utxo, sender_randomness) + symmetric_key.generate_public_announcement(utxo_notification_payload) } } } @@ -486,9 +487,13 @@ mod test { .is_empty()); // 6. generate a public announcement for this address + let utxo_notification_payload = UtxoNotificationPayload { + utxo: utxo.clone(), + sender_randomness, + }; let public_announcement = key .to_address() - .generate_public_announcement(&utxo, sender_randomness) + .generate_public_announcement(utxo_notification_payload) .unwrap(); // 7. verify that the public_announcement is marked as our key type. diff --git a/src/models/state/wallet/address/generation_address.rs b/src/models/state/wallet/address/generation_address.rs index 4f483947a..215d7de92 100644 --- a/src/models/state/wallet/address/generation_address.rs +++ b/src/models/state/wallet/address/generation_address.rs @@ -36,6 +36,7 @@ use crate::config_models::network::Network; use crate::models::blockchain::shared::Hash; use crate::models::blockchain::transaction::lock_script::LockScript; use crate::models::blockchain::transaction::lock_script::LockScriptAndWitness; +use crate::models::blockchain::transaction::transaction_output::UtxoNotificationPayload; use crate::models::blockchain::transaction::utxo::Utxo; use crate::models::blockchain::transaction::PublicAnnouncement; use crate::prelude::twenty_first; @@ -142,6 +143,17 @@ impl GenerationSpendingKey { fn generate_spending_lock(&self) -> Digest { self.unlock_key.hash() } + + pub(crate) fn try_decrypt_to_utxo_notification( + &self, + public_announcement: PublicAnnouncement, + ) -> Result { + let (utxo, sender_randomness) = self.decrypt(&public_announcement.message)?; + Ok(UtxoNotificationPayload { + utxo, + sender_randomness, + }) + } } impl GenerationReceivingAddress { @@ -262,12 +274,15 @@ impl GenerationReceivingAddress { pub(crate) fn generate_public_announcement( &self, - utxo: &Utxo, - sender_randomness: Digest, + utxo_notification_payload: UtxoNotificationPayload, ) -> Result { let ciphertext = [ &[GENERATION_FLAG_U8.into(), self.receiver_identifier], - self.encrypt(utxo, sender_randomness)?.as_slice(), + self.encrypt( + &utxo_notification_payload.utxo, + utxo_notification_payload.sender_randomness, + )? + .as_slice(), ] .concat(); diff --git a/src/models/state/wallet/address/symmetric_key.rs b/src/models/state/wallet/address/symmetric_key.rs index b1159beba..47a393c18 100644 --- a/src/models/state/wallet/address/symmetric_key.rs +++ b/src/models/state/wallet/address/symmetric_key.rs @@ -18,6 +18,7 @@ use super::common; use crate::models::blockchain::shared::Hash; use crate::models::blockchain::transaction::lock_script::LockScript; use crate::models::blockchain::transaction::lock_script::LockScriptAndWitness; +use crate::models::blockchain::transaction::transaction_output::UtxoNotificationPayload; use crate::models::blockchain::transaction::utxo::Utxo; use crate::models::blockchain::transaction::PublicAnnouncement; use crate::prelude::twenty_first; @@ -195,12 +196,15 @@ impl SymmetricKey { pub(crate) fn generate_public_announcement( &self, - utxo: &Utxo, - sender_randomness: Digest, + utxo_notification_payload: UtxoNotificationPayload, ) -> Result { let ciphertext = [ &[SYMMETRIC_KEY_FLAG_U8.into(), self.receiver_identifier()], - self.encrypt(utxo, sender_randomness)?.as_slice(), + self.encrypt( + &utxo_notification_payload.utxo, + utxo_notification_payload.sender_randomness, + )? + .as_slice(), ] .concat(); diff --git a/src/models/state/wallet/expected_utxo.rs b/src/models/state/wallet/expected_utxo.rs index 96abc8cf6..8ecc6cb74 100644 --- a/src/models/state/wallet/expected_utxo.rs +++ b/src/models/state/wallet/expected_utxo.rs @@ -13,8 +13,14 @@ use crate::util_types::mutator_set::commit; /// represents utxo and secrets necessary for recipient to claim it. /// -/// [ExpectedUtxo] is intended for offchain temporary storage of utxos that a -/// wallet sends to itself, eg change outputs. +/// [ExpectedUtxo] is intended to inform the wallet of UTXOs that were confirmed +/// or are about to be confirmed, that it can claim. For example: +/// - change outputs, +/// - coinbase UTXOs (produced by the miner) +/// - incoming off-chain transaction notifications. +/// +/// The on-chain notifications follow a completely different code path and never +/// touch [ExpectedUtxo]s. /// /// The `ExpectedUtxo` will exist in the local /// [RustyWalletDatabase](super::rusty_wallet_database::RustyWalletDatabase) @@ -29,6 +35,9 @@ use crate::util_types::mutator_set::commit; /// blockchain space and may leak some privacy if a key is ever used more than /// once. /// +/// Objects of this type are not intended to be transmitted; they only ever live +/// locally in the client's memory or disk. The main use of this thing +/// /// ### about `receiver_preimage` /// /// See issue #176. @@ -69,7 +78,9 @@ impl ExpectedUtxo { } } -#[derive(Clone, Debug, PartialEq, Eq, Hash, GetSize, Serialize, Deserialize)] +#[derive( + Clone, Debug, PartialEq, Eq, Hash, GetSize, Serialize, Deserialize, strum_macros::Display, +)] pub enum UtxoNotifier { OwnMiner, Cli, diff --git a/src/models/state/wallet/mod.rs b/src/models/state/wallet/mod.rs index 0788502f0..b7e36de2c 100644 --- a/src/models/state/wallet/mod.rs +++ b/src/models/state/wallet/mod.rs @@ -871,7 +871,7 @@ mod wallet_tests { ); let utxo = Utxo::new_native_coin(LockScript::anyone_can_spend(), NeptuneCoins::new(15)); - let tx_outputs = vec![TxOutput::fake_address( + let tx_outputs = vec![TxOutput::onchain( utxo, random(), other_wallet_recipient_address.privacy_digest, diff --git a/src/models/state/wallet/wallet_state.rs b/src/models/state/wallet/wallet_state.rs index d18cc8d4c..413e530cc 100644 --- a/src/models/state/wallet/wallet_state.rs +++ b/src/models/state/wallet/wallet_state.rs @@ -886,7 +886,7 @@ impl WalletState { /// must include fees that are paid in the transaction. pub(crate) async fn allocate_sufficient_input_funds( &self, - requested_amount: NeptuneCoins, + total_spend: NeptuneCoins, tip_digest: Digest, timestamp: Timestamp, ) -> Result> { @@ -896,10 +896,10 @@ impl WalletState { let wallet_status = self.get_wallet_status_from_lock(tip_digest).await; // First check that we have enough. Otherwise return an error. - if wallet_status.synced_unspent_available_amount(timestamp) < requested_amount { + if wallet_status.synced_unspent_available_amount(timestamp) < total_spend { bail!( "Insufficient synced amount to create transaction. Requested: {}, Total synced UTXOs: {}. Total synced amount: {}. Synced unspent available amount: {}. Synced unspent timelocked amount: {}. Total unsynced UTXOs: {}. Unsynced unspent amount: {}. Block is: {}", - requested_amount, + total_spend, wallet_status.synced_unspent.len(), wallet_status.synced_unspent.iter().map(|(wse, _msmp)| wse.utxo.get_native_currency_amount()).sum::(), wallet_status.synced_unspent_available_amount(timestamp), @@ -911,7 +911,7 @@ impl WalletState { let mut ret = vec![]; let mut allocated_amount = NeptuneCoins::zero(); - while allocated_amount < requested_amount { + while allocated_amount < total_spend { let (wallet_status_element, membership_proof) = wallet_status.synced_unspent[ret.len()].clone(); @@ -926,7 +926,6 @@ impl WalletState { continue; } }; - let lock_script = spending_key.to_address().lock_script(); allocated_amount = allocated_amount + wallet_status_element.utxo.get_native_currency_amount(); diff --git a/src/peer_loop.rs b/src/peer_loop.rs index 6ccca3261..f3c8b5065 100644 --- a/src/peer_loop.rs +++ b/src/peer_loop.rs @@ -1199,9 +1199,11 @@ mod peer_loop_tests { use super::*; use crate::config_models::network::Network; + use crate::models::blockchain::transaction::transaction_output::UtxoNotifyMethod; use crate::models::blockchain::type_scripts::neptune_coins::NeptuneCoins; use crate::models::peer::TransactionNotification; use crate::models::state::tx_proving_capability::TxProvingCapability; + use crate::models::state::wallet::address::KeyType; use crate::models::state::wallet::WalletSecret; use crate::tests::shared::get_dummy_peer_connection_data_genesis; use crate::tests::shared::get_dummy_socket_address; @@ -2453,13 +2455,32 @@ mod peer_loop_tests { #[traced_test] #[tokio::test] - async fn populated_mempool_request_tx_test() -> Result<()> { + async fn populated_mempool_request_tx_test() { let network = Network::Main; // In this scenario the peer is informed of a transaction that it already knows let (_peer_broadcast_tx, from_main_rx_clone, to_main_tx, mut to_main_rx1, state_lock, _hsd) = - get_test_genesis_setup(network, 1).await?; + get_test_genesis_setup(network, 1).await.unwrap(); + let spending_key = state_lock + .lock_guard() + .await + .wallet_state + .next_unused_spending_key(KeyType::Symmetric); - let transaction_1 = make_mock_transaction(vec![], vec![]); + let genesis_block = Block::genesis_block(network); + let now = genesis_block.kernel.header.timestamp; + let transaction_1 = state_lock + .lock_guard_mut() + .await + .create_transaction_with_prover_capability( + &mut Default::default(), + spending_key, + UtxoNotifyMethod::OffChain, + NeptuneCoins::new(0), + now, + TxProvingCapability::ProofCollection, + ) + .await + .unwrap(); // Build the resulting transaction notification let tx_notification: TransactionNotification = transaction_1.clone().into(); @@ -2479,20 +2500,6 @@ mod peer_loop_tests { ); let mut peer_state = MutablePeerState::new(hsd_1.tip_header.height); - let genesis_block = Block::genesis_block(network); - let now = genesis_block.kernel.header.timestamp; - let transaction_1 = state_lock - .lock_guard_mut() - .await - .create_transaction_with_prover_capability( - vec![], - NeptuneCoins::new(0), - now, - TxProvingCapability::ProofCollection, - ) - .await - .unwrap(); - assert!( state_lock.lock_guard().await.mempool.is_empty(), "Mempool must be empty at init" @@ -2507,23 +2514,16 @@ mod peer_loop_tests { "Mempool must be non-empty after insertion" ); - // Build the resulting transaction notification - let tx_notification: TransactionNotification = transaction_1.clone().into(); - let mock = Mock::new(vec![ - Action::Read(PeerMessage::TransactionNotification(tx_notification)), - Action::Read(PeerMessage::Bye), - ]); peer_loop_handler .run(mock, from_main_rx_clone, &mut peer_state) - .await?; + .await + .unwrap(); - // nothing may be sent to `main_loop` + // nothing is allowed to be sent to `main_loop` match to_main_rx1.try_recv() { Err(TryRecvError::Empty) => (), - Err(TryRecvError::Disconnected) => bail!("to_main channel must still be open"), - Ok(_) => bail!("to_main channel must be empty"), - } - - Ok(()) + Err(TryRecvError::Disconnected) => panic!("to_main channel must still be open"), + Ok(_) => panic!("to_main channel must be empty"), + }; } } diff --git a/src/rpc_server.rs b/src/rpc_server.rs index 94c0b4d88..973feef60 100644 --- a/src/rpc_server.rs +++ b/src/rpc_server.rs @@ -12,6 +12,7 @@ use std::str::FromStr; use anyhow::Result; use get_size::GetSize; +use itertools::Itertools; use serde::Deserialize; use serde::Serialize; use systemstat::Platform; @@ -39,6 +40,8 @@ use crate::models::proof_abstractions::timestamp::Timestamp; use crate::models::state::wallet::address::KeyType; use crate::models::state::wallet::address::ReceivingAddress; use crate::models::state::wallet::coin_with_possible_timelock::CoinWithPossibleTimeLock; +use crate::models::state::wallet::expected_utxo::ExpectedUtxo; +use crate::models::state::wallet::expected_utxo::UtxoNotifier; use crate::models::state::wallet::wallet_status::WalletStatus; use crate::models::state::GlobalStateLock; use crate::prelude::twenty_first; @@ -650,7 +653,7 @@ impl RPC for NeptuneRPCServer { }; let state = self.state.lock_guard().await; - let mut tx_outputs = match state.generate_tx_outputs(outputs, owned_utxo_notify_method) { + let tx_outputs = match state.generate_tx_outputs(outputs, owned_utxo_notify_method) { Ok(u) => u, Err(err) => { tracing::error!("Could not generate tx outputs: {}", err); @@ -675,9 +678,9 @@ impl RPC for NeptuneRPCServer { // lengthy operation. // // note: A change output will be added to tx_outputs if needed. - let transaction = match state + let (transaction, maybe_change_output) = match state .create_transaction( - &mut tx_outputs, + tx_outputs.clone(), change_key, owned_utxo_notify_method, fee, @@ -693,17 +696,45 @@ impl RPC for NeptuneRPCServer { }; drop(state); + let mut utxos_sent_to_self = { + let wallet = &self.state.lock_guard().await.wallet_state; + tx_outputs + .iter() + .filter(|txo| txo.is_offchain()) + .filter_map(|txo| { + wallet + .find_spending_key_for_utxo(&txo.utxo) + .map(|sk| (txo, sk)) + }) + .map(|(tx_output, spending_key)| { + ExpectedUtxo::new( + tx_output.utxo.clone(), + tx_output.sender_randomness, + spending_key.privacy_preimage(), + UtxoNotifier::Myself, + ) + }) + .collect_vec() + }; + + if let Some(change_output) = maybe_change_output { + let expected_change_utxo = ExpectedUtxo::new( + change_output.utxo, + change_output.sender_randomness, + change_key.privacy_preimage(), + UtxoNotifier::Myself, + ); + utxos_sent_to_self.push(expected_change_utxo); + } + // if the tx created offchain expected_utxos we must inform wallet. - if tx_outputs.has_offchain() { + if !utxos_sent_to_self.is_empty() { // acquire write-lock let mut gsm = self.state.lock_guard_mut().await; // Inform wallet of any expected incoming utxos. // note that this (briefly) mutates self. - if let Err(e) = gsm - .add_expected_utxos_to_wallet(tx_outputs.expected_utxos_iter()) - .await - { + if let Err(e) = gsm.add_expected_utxos_to_wallet(utxos_sent_to_self).await { tracing::error!("Could not add expected utxos to wallet: {}", e); return None; } @@ -715,7 +746,7 @@ impl RPC for NeptuneRPCServer { // Send transaction message to main let response: Result<(), SendError> = self .rpc_server_to_main_tx - .send(RPCServerToMain::Send(Box::new(transaction.clone()))) + .send(RPCServerToMain::BroadcastTx(Box::new(transaction.clone()))) .await; // Restart mining if it was paused diff --git a/src/tests/shared.rs b/src/tests/shared.rs index 17a33d5f4..f81b216bf 100644 --- a/src/tests/shared.rs +++ b/src/tests/shared.rs @@ -61,6 +61,7 @@ use crate::models::blockchain::transaction::transaction_kernel::pseudorandom_opt use crate::models::blockchain::transaction::transaction_kernel::pseudorandom_public_announcement; use crate::models::blockchain::transaction::transaction_kernel::pseudorandom_transaction_kernel; use crate::models::blockchain::transaction::transaction_kernel::TransactionKernel; +use crate::models::blockchain::transaction::transaction_output::TxOutputList; use crate::models::blockchain::transaction::utxo::Utxo; use crate::models::blockchain::transaction::PublicAnnouncement; use crate::models::blockchain::transaction::Transaction; @@ -735,8 +736,7 @@ pub(crate) async fn make_mock_transaction_with_generation_key( Transaction { kernel, proof } } -// `make_mock_transaction`, in contrast to `make_mock_transaction2`, assumes you -// already have created `DevNetInput`s. +/// Make a transaction with `Invalid` transaction proof. pub fn make_mock_transaction( inputs: Vec, outputs: Vec, @@ -751,7 +751,7 @@ pub fn make_mock_transaction( fee: NeptuneCoins::new(1), timestamp, coinbase: None, - mutator_set_hash: random(), + mutator_set_hash: Digest::default(), }, proof: TransactionProof::Invalid, }