From da3d8641cac64d4d305c292403b817766721a4ea Mon Sep 17 00:00:00 2001 From: SW van Heerden Date: Wed, 17 Nov 2021 14:46:10 +0200 Subject: [PATCH] Add atomic swap refund add tx priority add script context --- applications/tari_app_grpc/proto/types.proto | 2 + applications/tari_app_grpc/proto/wallet.proto | 13 +- .../src/conversions/unblinded_output.rs | 2 + applications/tari_base_node/src/builder.rs | 1 + .../src/automation/command_parser.rs | 14 + .../src/automation/commands.rs | 27 ++ .../src/grpc/wallet_grpc_server.rs | 56 +++- .../unconfirmed_pool/unconfirmed_pool.rs | 8 +- .../core/src/transactions/aggregated_body.rs | 15 +- .../core/src/transactions/coinbase_builder.rs | 7 +- .../core/src/transactions/test_helpers.rs | 14 +- .../core/src/transactions/transaction.rs | 50 +++- .../transaction_protocol/sender.rs | 50 ++-- .../transaction_initializer.rs | 34 ++- .../block_validators/async_validator.rs | 11 +- base_layer/core/src/validation/helpers.rs | 2 + .../src/validation/transaction_validators.rs | 23 +- base_layer/core/tests/mempool.rs | 4 +- base_layer/core/tests/node_comms_interface.rs | 1 + .../down.sql | 0 .../up.sql | 31 +++ .../mock_base_node_service.rs | 2 +- .../src/output_manager_service/handle.rs | 66 +++-- .../recovery/standard_outputs_recoverer.rs | 4 +- .../src/output_manager_service/service.rs | 247 ++++++++++++------ .../storage/database.rs | 51 +++- .../output_manager_service/storage/models.rs | 32 +++ .../storage/sqlite_db/mod.rs | 62 ++++- .../storage/sqlite_db/output_sql.rs | 71 ++++- base_layer/wallet/src/schema.rs | 3 + .../protocols/transaction_receive_protocol.rs | 15 +- .../protocols/transaction_send_protocol.rs | 18 +- .../wallet/src/transaction_service/service.rs | 96 ++++--- .../transaction_service/storage/sqlite_db.rs | 2 +- base_layer/wallet/src/wallet.rs | 7 +- .../tests/output_manager_service/service.rs | 117 +++++---- .../tests/output_manager_service/storage.rs | 12 +- base_layer/wallet/tests/support/comms_rpc.rs | 2 +- .../tests/transaction_service/service.rs | 102 ++++---- .../tests/transaction_service/storage.rs | 2 +- base_layer/wallet/tests/wallet/mod.rs | 7 +- base_layer/wallet_ffi/src/lib.rs | 2 + clients/wallet_grpc_client/index.js | 1 + .../features/WalletTransfer.feature | 20 +- integration_tests/features/support/steps.js | 80 +++++- integration_tests/helpers/walletClient.js | 4 + 46 files changed, 1054 insertions(+), 336 deletions(-) create mode 100644 base_layer/wallet/migrations/2021-11-11-094000_add_script_lock_height/down.sql create mode 100644 base_layer/wallet/migrations/2021-11-11-094000_add_script_lock_height/up.sql diff --git a/applications/tari_app_grpc/proto/types.proto b/applications/tari_app_grpc/proto/types.proto index 49306660b7..34db0289e5 100644 --- a/applications/tari_app_grpc/proto/types.proto +++ b/applications/tari_app_grpc/proto/types.proto @@ -288,6 +288,8 @@ message UnblindedOutput { bytes sender_offset_public_key = 8; // UTXO signature with the script offset private key, k_O ComSignature metadata_signature = 9; + // The minimum height the script allows this output to be spent + uint64 script_lock_height = 10; } // ----------------------------- Network Types ----------------------------- // diff --git a/applications/tari_app_grpc/proto/wallet.proto b/applications/tari_app_grpc/proto/wallet.proto index 4aee184a72..99319145e8 100644 --- a/applications/tari_app_grpc/proto/wallet.proto +++ b/applications/tari_app_grpc/proto/wallet.proto @@ -54,12 +54,14 @@ service Wallet { rpc ListConnectedPeers(Empty) returns (ListConnectedPeersResponse); // Cancel pending transaction rpc CancelTransaction (CancelTransactionRequest) returns (CancelTransactionResponse); - // Will triggger a complete revalidation of all wallet outputs. + // Will trigger a complete revalidation of all wallet outputs. rpc RevalidateAllTransactions (RevalidateRequest) returns (RevalidateResponse); // This will send a XTR SHA Atomic swap transaction rpc SendShaAtomicSwapTransaction(SendShaAtomicSwapRequest) returns (SendShaAtomicSwapResponse); // This will claim a XTR SHA Atomic swap transaction rpc ClaimShaAtomicSwapTransaction(ClaimShaAtomicSwapRequest) returns (ClaimShaAtomicSwapResponse); + // This will claim a HTLC refund transaction + rpc ClaimHtlcRefundTransaction(ClaimHtlcRefundRequest) returns (ClaimHtlcRefundResponse); } message GetVersionRequest { } @@ -125,6 +127,15 @@ message ClaimShaAtomicSwapResponse { TransferResult results = 1; } +message ClaimHtlcRefundRequest{ + string output_hash = 1; + uint64 fee_per_gram = 2; +} + +message ClaimHtlcRefundResponse { + TransferResult results = 1; +} + message GetTransactionInfoRequest { repeated uint64 transaction_ids = 1; } diff --git a/applications/tari_app_grpc/src/conversions/unblinded_output.rs b/applications/tari_app_grpc/src/conversions/unblinded_output.rs index bf9efa58bc..f452928e12 100644 --- a/applications/tari_app_grpc/src/conversions/unblinded_output.rs +++ b/applications/tari_app_grpc/src/conversions/unblinded_output.rs @@ -49,6 +49,7 @@ impl From for grpc::UnblindedOutput { signature_u: Vec::from(output.metadata_signature.u().as_bytes()), signature_v: Vec::from(output.metadata_signature.v().as_bytes()), }), + script_lock_height: output.script_lock_height, } } } @@ -91,6 +92,7 @@ impl TryFrom for UnblindedOutput { script_private_key, sender_offset_public_key, metadata_signature, + script_lock_height: output.script_lock_height, }) } } diff --git a/applications/tari_base_node/src/builder.rs b/applications/tari_base_node/src/builder.rs index ca7d6d2cab..bdacc47765 100644 --- a/applications/tari_base_node/src/builder.rs +++ b/applications/tari_base_node/src/builder.rs @@ -246,6 +246,7 @@ async fn build_node_context( Box::new(TxInternalConsistencyValidator::new( factories.clone(), config.base_node_bypass_range_proof_verification, + blockchain_db.clone(), )), Box::new(TxInputAndMaturityValidator::new(blockchain_db.clone())), Box::new(TxConsensusValidator::new(blockchain_db.clone())), diff --git a/applications/tari_console_wallet/src/automation/command_parser.rs b/applications/tari_console_wallet/src/automation/command_parser.rs index 89c90c6aac..0ed8acf4a6 100644 --- a/applications/tari_console_wallet/src/automation/command_parser.rs +++ b/applications/tari_console_wallet/src/automation/command_parser.rs @@ -59,6 +59,7 @@ impl Display for ParsedCommand { ClearCustomBaseNode => "clear-custom-base-node", InitShaAtomicSwap => "init-sha-atomic-swap", FinaliseShaAtomicSwap => "finalise-sha-atomic-swap", + ClaimShaAtomicSwapRefund => "claim-sha-atomic-swap-refund", }; let args = self @@ -130,6 +131,7 @@ pub fn parse_command(command: &str) -> Result { ClearCustomBaseNode => Vec::new(), InitShaAtomicSwap => parse_init_sha_atomic_swap(args)?, FinaliseShaAtomicSwap => parse_finalise_sha_atomic_swap(args)?, + ClaimShaAtomicSwapRefund => parse_claim_htlc_refund_refund(args)?, }; Ok(ParsedCommand { command, args }) @@ -219,6 +221,18 @@ fn parse_finalise_sha_atomic_swap(mut args: SplitWhitespace) -> Result Result, ParseError> { + let mut parsed_args = Vec::new(); + // hash + let hash = args + .next() + .ok_or_else(|| ParseError::Empty("Output hash".to_string()))?; + let hash = parse_hash(hash).ok_or(ParseError::Hash)?; + parsed_args.push(ParsedArgument::Hash(hash)); + + Ok(parsed_args) +} + fn parse_make_it_rain(mut args: SplitWhitespace) -> Result, ParseError> { let mut parsed_args = Vec::new(); diff --git a/applications/tari_console_wallet/src/automation/commands.rs b/applications/tari_console_wallet/src/automation/commands.rs index 58a7d80a31..8468ec8a18 100644 --- a/applications/tari_console_wallet/src/automation/commands.rs +++ b/applications/tari_console_wallet/src/automation/commands.rs @@ -88,6 +88,7 @@ pub enum WalletCommand { ClearCustomBaseNode, InitShaAtomicSwap, FinaliseShaAtomicSwap, + ClaimShaAtomicSwapRefund, } #[derive(Debug, EnumString, PartialEq, Clone)] @@ -205,6 +206,27 @@ pub async fn finalise_sha_atomic_swap( Ok(tx_id) } +/// claims a HTLC refund transaction +pub async fn claim_htlc_refund( + mut output_service: OutputManagerHandle, + mut transaction_service: TransactionServiceHandle, + args: Vec, +) -> Result { + use ParsedArgument::*; + let output = match args[0].clone() { + Hash(output) => Ok(output), + _ => Err(CommandError::Argument), + }?; + + let (tx_id, fee, amount, tx) = output_service + .create_htlc_refund_transaction(output, MicroTari(25)) + .await?; + transaction_service + .submit_transaction(tx_id, tx, fee, amount, "Claimed HTLC refund".into()) + .await?; + Ok(tx_id) +} + /// Send a one-sided transaction to a recipient pub async fn send_one_sided( mut wallet_transaction_service: TransactionServiceHandle, @@ -759,6 +781,11 @@ pub async fn command_runner( debug!(target: LOG_TARGET, "claiming tari HTLC tx_id {}", tx_id); tx_ids.push(tx_id); }, + ClaimShaAtomicSwapRefund => { + let tx_id = claim_htlc_refund(output_service.clone(), transaction_service.clone(), parsed.args).await?; + debug!(target: LOG_TARGET, "claiming tari HTLC tx_id {}", tx_id); + tx_ids.push(tx_id); + }, } } 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 129cf06da8..18e19d6a18 100644 --- a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -7,6 +7,8 @@ use tari_app_grpc::{ tari_rpc::{ payment_recipient::PaymentType, wallet_server, + ClaimHtlcRefundRequest, + ClaimHtlcRefundResponse, ClaimShaAtomicSwapRequest, ClaimShaAtomicSwapResponse, CoinSplitRequest, @@ -189,7 +191,7 @@ impl wallet_server::Wallet for WalletGrpcServer { "Transaction broadcast: {}, preimage_hex: {}, hash {}", tx_id, pre_image.to_hex(), - output.to_string() + output.hash().to_hex() ); SendShaAtomicSwapResponse { transaction_id: tx_id, @@ -226,7 +228,7 @@ impl wallet_server::Wallet for WalletGrpcServer { .map_err(|_| Status::internal("pre_image is malformed".to_string()))?; let output = BlockHash::from_hex(&message.output) .map_err(|_| Status::internal("Output hash is malformed".to_string()))?; - + debug!(target: LOG_TARGET, "Trying to claim HTLC with hash {}", output.to_hex()); let mut transaction_service = self.get_transaction_service(); let mut output_manager_service = self.get_output_manager_service(); let response = match output_manager_service @@ -274,6 +276,56 @@ impl wallet_server::Wallet for WalletGrpcServer { })) } + async fn claim_htlc_refund_transaction( + &self, + request: Request, + ) -> Result, Status> { + let message = request.into_inner(); + let output = BlockHash::from_hex(&message.output_hash) + .map_err(|_| Status::internal("Output hash is malformed".to_string()))?; + + let mut transaction_service = self.get_transaction_service(); + let mut output_manager_service = self.get_output_manager_service(); + debug!(target: LOG_TARGET, "Trying to claim HTLC with hash {}", output.to_hex()); + let response = match output_manager_service + .create_htlc_refund_transaction(output, message.fee_per_gram.into()) + .await + { + Ok((tx_id, fee, amount, tx)) => { + match transaction_service + .submit_transaction(tx_id, tx, fee, amount, "Creating HTLC refund transaction".to_string()) + .await + { + Ok(()) => TransferResult { + address: Default::default(), + transaction_id: tx_id, + is_success: true, + failure_message: Default::default(), + }, + Err(e) => TransferResult { + address: Default::default(), + transaction_id: Default::default(), + is_success: false, + failure_message: e.to_string(), + }, + } + }, + Err(e) => { + warn!(target: LOG_TARGET, "Failed to claim HTLC refund transaction: {}", e); + TransferResult { + address: Default::default(), + transaction_id: Default::default(), + is_success: false, + failure_message: e.to_string(), + } + }, + }; + + Ok(Response::new(ClaimHtlcRefundResponse { + results: Some(response), + })) + } + async fn transfer(&self, request: Request) -> Result, Status> { let message = request.into_inner(); let recipients = message diff --git a/base_layer/core/src/mempool/unconfirmed_pool/unconfirmed_pool.rs b/base_layer/core/src/mempool/unconfirmed_pool/unconfirmed_pool.rs index a97d1e5773..5e979b296b 100644 --- a/base_layer/core/src/mempool/unconfirmed_pool/unconfirmed_pool.rs +++ b/base_layer/core/src/mempool/unconfirmed_pool/unconfirmed_pool.rs @@ -596,8 +596,12 @@ mod test { .unwrap(); let factories = CryptoFactories::default(); - let mut stx_protocol = stx_builder.build::(&factories).unwrap(); - stx_protocol.finalize(KernelFeatures::empty(), &factories).unwrap(); + let mut stx_protocol = stx_builder + .build::(&factories, None, Some(u64::MAX)) + .unwrap(); + stx_protocol + .finalize(KernelFeatures::empty(), &factories, None, Some(u64::MAX)) + .unwrap(); let tx3 = stx_protocol.get_transaction().unwrap().clone(); diff --git a/base_layer/core/src/transactions/aggregated_body.rs b/base_layer/core/src/transactions/aggregated_body.rs index 45a30c730b..f32fa6d4b8 100644 --- a/base_layer/core/src/transactions/aggregated_body.rs +++ b/base_layer/core/src/transactions/aggregated_body.rs @@ -41,12 +41,14 @@ use log::*; use serde::{Deserialize, Serialize}; use std::{ cmp::max, + convert::TryInto, fmt::{Display, Error, Formatter}, }; use tari_common_types::types::{ BlindingFactor, Commitment, CommitmentFactory, + HashOutput, PrivateKey, PublicKey, RangeProofService, @@ -55,6 +57,7 @@ use tari_crypto::{ commitment::HomomorphicCommitmentFactory, keys::PublicKey as PublicKeyTrait, ristretto::pedersen::PedersenCommitment, + script::ScriptContext, tari_utilities::hex::Hex, }; @@ -342,6 +345,7 @@ impl AggregateBody { /// This function does NOT check that inputs come from the UTXO set /// The reward is the total amount of Tari rewarded for this block (block reward + total fees), this should be 0 /// for a transaction + #[allow(clippy::too_many_arguments)] pub fn validate_internal_consistency( &self, tx_offset: &BlindingFactor, @@ -349,6 +353,8 @@ impl AggregateBody { bypass_range_proof_verification: bool, total_reward: MicroTari, factories: &CryptoFactories, + prev_header: Option, + height: Option, ) -> Result<(), TransactionError> { self.verify_kernel_signatures()?; @@ -361,7 +367,7 @@ impl AggregateBody { self.verify_metadata_signatures()?; let script_offset_g = PublicKey::from_secret_key(script_offset); - self.validate_script_offset(script_offset_g, &factories.commitment) + self.validate_script_offset(script_offset_g, &factories.commitment, prev_header, height) } pub fn dissolve(self) -> (Vec, Vec, Vec) { @@ -425,12 +431,17 @@ impl AggregateBody { &self, script_offset: PublicKey, factory: &CommitmentFactory, + prev_header: Option, + height: Option, ) -> Result<(), TransactionError> { trace!(target: LOG_TARGET, "Checking script offset"); // lets count up the input script public keys let mut input_keys = PublicKey::default(); + let prev_hash: [u8; 32] = prev_header.unwrap_or_default().as_slice().try_into().unwrap_or([0; 32]); + let height = height.unwrap_or_default(); for input in &self.inputs { - input_keys = input_keys + input.run_and_verify_script(factory)?; + let context = ScriptContext::new(height, &prev_hash, &input.commitment); + input_keys = input_keys + input.run_and_verify_script(factory, Some(context))?; } // Now lets gather the output public keys and hashes. diff --git a/base_layer/core/src/transactions/coinbase_builder.rs b/base_layer/core/src/transactions/coinbase_builder.rs index 3c75d7fa84..c34153c2bd 100644 --- a/base_layer/core/src/transactions/coinbase_builder.rs +++ b/base_layer/core/src/transactions/coinbase_builder.rs @@ -208,6 +208,7 @@ impl CoinbaseBuilder { script_private_key, sender_offset_public_key, metadata_sig, + 0, ); let output = if let Some(rewind_data) = self.rewind_data.as_ref() { unblinded_output @@ -235,7 +236,7 @@ impl CoinbaseBuilder { .with_reward(total_reward) .with_kernel(kernel); let tx = builder - .build(&self.factories) + .build(&self.factories, None, Some(height)) .map_err(|e| CoinbaseBuildError::BuildError(e.to_string()))?; Ok((tx, unblinded_output)) } @@ -525,7 +526,9 @@ mod test { &PrivateKey::default(), false, block_reward, - &factories + &factories, + None, + Some(u64::MAX) ), Ok(()) ); diff --git a/base_layer/core/src/transactions/test_helpers.rs b/base_layer/core/src/transactions/test_helpers.rs index 4fe0be260f..7a8c9dbf76 100644 --- a/base_layer/core/src/transactions/test_helpers.rs +++ b/base_layer/core/src/transactions/test_helpers.rs @@ -155,6 +155,7 @@ impl TestParams { self.script_private_key.clone(), self.sender_offset_public_key.clone(), metadata_signature, + 0, ) } @@ -444,8 +445,10 @@ pub fn create_transaction_with( stx_builder.with_output(utxo, script_offset_pvt_key).unwrap(); }); - let mut stx_protocol = stx_builder.build::(&factories).unwrap(); - stx_protocol.finalize(KernelFeatures::empty(), &factories).unwrap(); + let mut stx_protocol = stx_builder.build::(&factories, None, Some(u64::MAX)).unwrap(); + stx_protocol + .finalize(KernelFeatures::empty(), &factories, None, Some(u64::MAX)) + .unwrap(); stx_protocol.take_transaction().unwrap() } @@ -513,7 +516,7 @@ pub fn spend_utxos(schema: TransactionSchema) -> (Transaction, Vec(&factories).unwrap(); + let mut stx_protocol = stx_builder.build::(&factories, None, Some(u64::MAX)).unwrap(); let change = stx_protocol.get_change_amount().unwrap(); // The change output is assigned its own random script offset private key let change_sender_offset_public_key = stx_protocol.get_change_sender_offset_public_key().unwrap().unwrap(); @@ -539,9 +542,12 @@ pub fn spend_utxos(schema: TransactionSchema) -> (Transaction, Vec UnblindedOutput { UnblindedOutput { value, @@ -326,6 +331,7 @@ impl UnblindedOutput { script_private_key, sender_offset_public_key, metadata_signature, + script_lock_height, } } @@ -534,8 +540,9 @@ impl TransactionInput { /// This will run the script contained in the TransactionInput, returning either a script error or the resulting /// public key. - pub fn run_script(&self) -> Result { - match self.script.execute(&self.input_data)? { + pub fn run_script(&self, context: Option) -> Result { + let context = context.unwrap_or_default(); + match self.script.execute_with_context(&self.input_data, &context)? { StackItem::PublicKey(pubkey) => Ok(pubkey), _ => Err(TransactionError::ScriptExecutionError( "The script executed successfully but it did not leave a public key on the stack".to_string(), @@ -569,8 +576,12 @@ impl TransactionInput { /// This will run the script and verify the script signature. If its valid, it will return the resulting public key /// from the script. - pub fn run_and_verify_script(&self, factory: &CommitmentFactory) -> Result { - let key = self.run_script()?; + pub fn run_and_verify_script( + &self, + factory: &CommitmentFactory, + context: Option, + ) -> Result { + let key = self.run_script(context)?; self.validate_script_signature(&key, factory)?; Ok(key) } @@ -1236,6 +1247,8 @@ impl Transaction { bypass_range_proof_verification: bool, factories: &CryptoFactories, reward: Option, + prev_header: Option, + height: Option, ) -> Result<(), TransactionError> { let reward = reward.unwrap_or_else(|| 0 * uT); self.body.validate_internal_consistency( @@ -1244,6 +1257,8 @@ impl Transaction { bypass_range_proof_verification, reward, factories, + prev_header, + height, ) } @@ -1382,11 +1397,16 @@ impl TransactionBuilder { } /// Build the transaction. - pub fn build(self, factories: &CryptoFactories) -> Result { + pub fn build( + self, + factories: &CryptoFactories, + prev_header: Option, + height: Option, + ) -> Result { if let (Some(script_offset), Some(offset)) = (self.script_offset, self.offset) { let (i, o, k) = self.body.dissolve(); let tx = Transaction::new(i, o, k, offset, script_offset); - tx.validate_internal_consistency(true, factories, self.reward)?; + tx.validate_internal_consistency(true, factories, self.reward, prev_header, height)?; Ok(tx) } else { Err(TransactionError::ValidationError( @@ -1639,7 +1659,9 @@ mod test { let (tx, _, _) = test_helpers::create_tx(5000.into(), 3.into(), 1, 2, 1, 4); let factories = CryptoFactories::default(); - assert!(tx.validate_internal_consistency(false, &factories, None).is_ok()); + assert!(tx + .validate_internal_consistency(false, &factories, None, None, Some(u64::MAX)) + .is_ok()); } #[test] @@ -1652,7 +1674,9 @@ mod test { assert_eq!(tx.body.kernels().len(), 1); let factories = CryptoFactories::default(); - assert!(tx.validate_internal_consistency(false, &factories, None).is_ok()); + assert!(tx + .validate_internal_consistency(false, &factories, None, None, Some(u64::MAX)) + .is_ok()); let schema = txn_schema!(from: vec![outputs[1].clone()], to: vec![1 * T, 2 * T]); let (tx2, _outputs, _) = test_helpers::spend_utxos(schema); @@ -1683,11 +1707,13 @@ mod test { } // Validate basis transaction where cut-through has not been applied. - assert!(tx3.validate_internal_consistency(false, &factories, None).is_ok()); + assert!(tx3 + .validate_internal_consistency(false, &factories, None, None, Some(u64::MAX)) + .is_ok()); // tx3_cut_through has manual cut-through, it should not be possible so this should fail assert!(tx3_cut_through - .validate_internal_consistency(false, &factories, None) + .validate_internal_consistency(false, &factories, None, None, Some(u64::MAX)) .is_err()); } @@ -1725,7 +1751,9 @@ mod test { tx.body.inputs_mut()[0].input_data = stack; let factories = CryptoFactories::default(); - let err = tx.validate_internal_consistency(false, &factories, None).unwrap_err(); + let err = tx + .validate_internal_consistency(false, &factories, None, None, Some(u64::MAX)) + .unwrap_err(); assert!(matches!(err, TransactionError::InvalidSignatureError(_))); } diff --git a/base_layer/core/src/transactions/transaction_protocol/sender.rs b/base_layer/core/src/transactions/transaction_protocol/sender.rs index b38577a2ad..3d626270ac 100644 --- a/base_layer/core/src/transactions/transaction_protocol/sender.rs +++ b/base_layer/core/src/transactions/transaction_protocol/sender.rs @@ -50,7 +50,15 @@ use crate::{ use digest::Digest; use serde::{Deserialize, Serialize}; use std::fmt; -use tari_common_types::types::{BlindingFactor, ComSignature, PrivateKey, PublicKey, RangeProofService, Signature}; +use tari_common_types::types::{ + BlindingFactor, + ComSignature, + HashOutput, + PrivateKey, + PublicKey, + RangeProofService, + Signature, +}; use tari_crypto::{ keys::PublicKey as PublicKeyTrait, ristretto::pedersen::{PedersenCommitment, PedersenCommitmentFactory}, @@ -98,6 +106,8 @@ pub(super) struct RawTransactionInfo { pub recipient_info: RecipientInfo, pub signatures: Vec, pub message: String, + pub height: Option, + pub prev_header: Option, } #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] @@ -487,7 +497,9 @@ impl SenderTransactionProtocol { .with_signature(&s_agg) .build()?; tx_builder.with_kernel(kernel); - tx_builder.build(factories).map_err(TPE::from) + tx_builder + .build(factories, info.prev_header.clone(), info.height) + .map_err(TPE::from) } /// Performs sanity checks on the collected transaction pieces prior to building the final Transaction instance @@ -543,7 +555,13 @@ impl SenderTransactionProtocol { /// formally validate the transaction terms (no inflation, signature matches etc). If any step fails, /// the transaction protocol moves to Failed state and we are done; you can't rescue the situation. The function /// returns `Ok(false)` in this instance. - pub fn finalize(&mut self, features: KernelFeatures, factories: &CryptoFactories) -> Result<(), TPE> { + pub fn finalize( + &mut self, + features: KernelFeatures, + factories: &CryptoFactories, + prev_header: Option, + height: Option, + ) -> Result<(), TPE> { // Create the final aggregated signature, moving to the Failed state if anything goes wrong match &mut self.state { SenderState::Finalizing(_) => { @@ -566,7 +584,7 @@ impl SenderTransactionProtocol { } let transaction = result.unwrap(); let result = transaction - .validate_internal_consistency(true, factories, None) + .validate_internal_consistency(true, factories, None, prev_header, height) .map_err(TPE::TransactionBuildError); if let Err(e) = result { self.state = SenderState::Failed(e.clone()); @@ -840,10 +858,10 @@ mod test { p2.sender_offset_private_key.clone(), ) .unwrap(); - let mut sender = builder.build::(&factories).unwrap(); + let mut sender = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); assert!(!sender.is_failed()); assert!(sender.is_finalizing()); - match sender.finalize(KernelFeatures::empty(), &factories) { + match sender.finalize(KernelFeatures::empty(), &factories, None, Some(u64::MAX)) { Ok(_) => (), Err(e) => panic!("{:?}", e), } @@ -874,7 +892,7 @@ mod test { .with_change_script(script, ExecutionStack::default(), PrivateKey::default()) // A little twist: Check the case where the change is less than the cost of another output .with_amount(0, MicroTari(1200) - fee - MicroTari(10)); - let mut alice = builder.build::(&factories).unwrap(); + let mut alice = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); assert!(alice.is_single_round_message_ready()); let msg = alice.build_single_round_message().unwrap(); // Send message down the wire....and wait for response @@ -892,7 +910,7 @@ mod test { .unwrap(); // Transaction should be complete assert!(alice.is_finalizing()); - match alice.finalize(KernelFeatures::empty(), &factories) { + match alice.finalize(KernelFeatures::empty(), &factories, None, Some(u64::MAX)) { Ok(_) => (), Err(e) => panic!("{:?}", e), }; @@ -939,7 +957,7 @@ mod test { ) .with_change_script(script, ExecutionStack::default(), PrivateKey::default()) .with_amount(0, MicroTari(5000)); - let mut alice = builder.build::(&factories).unwrap(); + let mut alice = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); assert!(alice.is_single_round_message_ready()); let msg = alice.build_single_round_message().unwrap(); println!( @@ -973,7 +991,7 @@ mod test { .unwrap(); // Transaction should be complete assert!(alice.is_finalizing()); - match alice.finalize(KernelFeatures::empty(), &factories) { + match alice.finalize(KernelFeatures::empty(), &factories, None, Some(u64::MAX)) { Ok(_) => (), Err(e) => panic!("{:?}", e), }; @@ -987,7 +1005,7 @@ mod test { assert_eq!(tx.body.outputs().len(), 2); assert!(tx .clone() - .validate_internal_consistency(false, &factories, None) + .validate_internal_consistency(false, &factories, None, None, Some(u64::MAX)) .is_ok()); } @@ -1019,7 +1037,7 @@ mod test { ) .with_change_script(script, ExecutionStack::default(), PrivateKey::default()) .with_amount(0, (2u64.pow(32) + 1).into()); - let mut alice = builder.build::(&factories).unwrap(); + let mut alice = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); assert!(alice.is_single_round_message_ready()); let msg = alice.build_single_round_message().unwrap(); // Send message down the wire....and wait for response @@ -1062,7 +1080,7 @@ mod test { ) .with_change_script(script, ExecutionStack::default(), PrivateKey::default()); // Verify that the initial 'fee greater than amount' check rejects the transaction when it is constructed - match builder.build::(&factories) { + match builder.build::(&factories, None, Some(u64::MAX)) { Ok(_) => panic!("'BuildError(\"Fee is greater than amount\")' not caught"), Err(e) => assert_eq!(e.message, "Fee is greater than amount".to_string()), }; @@ -1095,7 +1113,7 @@ mod test { ) .with_change_script(script, ExecutionStack::default(), PrivateKey::default()); // Test if the transaction passes the initial 'fee greater than amount' check when it is constructed - match builder.build::(&factories) { + match builder.build::(&factories, None, Some(u64::MAX)) { Ok(_) => {}, Err(e) => panic!("Unexpected error: {:?}", e), }; @@ -1145,7 +1163,7 @@ mod test { PrivateKey::random(&mut OsRng), ) .with_change_script(script, ExecutionStack::default(), PrivateKey::default()); - let mut alice = builder.build::(&factories).unwrap(); + let mut alice = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); assert!(alice.is_single_round_message_ready()); let msg = alice.build_single_round_message().unwrap(); @@ -1173,7 +1191,7 @@ mod test { .unwrap(); // Transaction should be complete assert!(alice.is_finalizing()); - match alice.finalize(KernelFeatures::empty(), &factories) { + match alice.finalize(KernelFeatures::empty(), &factories, None, Some(u64::MAX)) { Ok(_) => (), Err(e) => panic!("{:?}", e), }; diff --git a/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs b/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs index dc445627d2..7657275782 100644 --- a/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs +++ b/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs @@ -49,7 +49,7 @@ use std::{ collections::HashMap, fmt::{Debug, Error, Formatter}, }; -use tari_common_types::types::{BlindingFactor, PrivateKey, PublicKey}; +use tari_common_types::types::{BlindingFactor, HashOutput, PrivateKey, PublicKey}; use tari_crypto::{ commitment::HomomorphicCommitmentFactory, keys::{PublicKey as PublicKeyTrait, SecretKey}, @@ -381,6 +381,7 @@ impl SenderTransactionInitializer { .clone(), PublicKey::from_secret_key(&change_sender_offset_private_key), metadata_signature, + 0, ); Ok((fee_with_change, v, Some(change_unblinded_output))) }, @@ -421,7 +422,12 @@ impl SenderTransactionInitializer { /// error (so that you can continue building) along with a string listing the missing fields. /// If all the input data is present, but one or more fields are invalid, the function will return a /// `SenderTransactionProtocol` instance in the Failed state. - pub fn build(mut self, factories: &CryptoFactories) -> Result { + pub fn build( + mut self, + factories: &CryptoFactories, + prev_header: Option, + height: Option, + ) -> Result { // Compile a list of all data that is missing let mut message = Vec::new(); Self::check_value("Missing Lock Height", &self.lock_height, &mut message); @@ -615,6 +621,8 @@ impl SenderTransactionInitializer { recipient_info, signatures: Vec::new(), message: self.message.unwrap_or_else(|| "".to_string()), + prev_header, + height, }; let state = SenderState::Initializing(Box::new(sender_info)); @@ -662,7 +670,7 @@ mod test { let p = TestParams::new(); // Start the builder let builder = SenderTransactionInitializer::new(0, create_consensus_constants(0)); - let err = builder.build::(&factories).unwrap_err(); + let err = builder.build::(&factories, None, Some(u64::MAX)).unwrap_err(); let script = script!(Nop); // We should have a bunch of fields missing still, but we can recover and continue assert_eq!( @@ -699,12 +707,12 @@ mod test { .with_change_script(script, ExecutionStack::default(), PrivateKey::default()); let expected_fee = builder.fee().calculate(MicroTari(20), 1, 1, 2, 0); // We needed a change input, so this should fail - let err = builder.build::(&factories).unwrap_err(); + let err = builder.build::(&factories, None, Some(u64::MAX)).unwrap_err(); assert_eq!(err.message, "Change spending key was not provided"); // Ok, give them a change output let mut builder = err.builder; builder.with_change_secret(p.change_spend_key); - let result = builder.build::(&factories).unwrap(); + let result = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); // Peek inside and check the results if let SenderState::Finalizing(info) = result.into_state() { assert_eq!(info.num_recipients, 0, "Number of receivers"); @@ -746,7 +754,7 @@ mod test { .with_input(utxo, input) .with_fee_per_gram(MicroTari(4)) .with_prevent_fee_gt_amount(false); - let result = builder.build::(&factories).unwrap(); + let result = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); // Peek inside and check the results if let SenderState::Finalizing(info) = result.into_state() { assert_eq!(info.num_recipients, 0, "Number of receivers"); @@ -797,7 +805,7 @@ mod test { .with_input(utxo, input) .with_fee_per_gram(MicroTari(1)) .with_prevent_fee_gt_amount(false); - let result = builder.build::(&factories).unwrap(); + let result = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); // Peek inside and check the results if let SenderState::Finalizing(info) = result.into_state() { assert_eq!(info.num_recipients, 0, "Number of receivers"); @@ -839,7 +847,7 @@ mod test { let (utxo, input) = create_test_input(MicroTari(50), 0, &factories.commitment); builder.with_input(utxo, input); } - let err = builder.build::(&factories).unwrap_err(); + let err = builder.build::(&factories, None, Some(u64::MAX)).unwrap_err(); assert_eq!(err.message, "Too many inputs in transaction"); } @@ -872,7 +880,7 @@ mod test { PrivateKey::random(&mut OsRng), ); // .with_change_script(script, ExecutionStack::default(), PrivateKey::default()); - let err = builder.build::(&factories).unwrap_err(); + let err = builder.build::(&factories, None, Some(u64::MAX)).unwrap_err(); assert_eq!(err.message, "Fee is less than the minimum"); } @@ -904,7 +912,7 @@ mod test { PrivateKey::random(&mut OsRng), ) .with_change_script(script, ExecutionStack::default(), PrivateKey::default()); - let err = builder.build::(&factories).unwrap_err(); + let err = builder.build::(&factories, None, Some(u64::MAX)).unwrap_err(); assert_eq!( err.message, "You are spending (471 µT) more than you're providing (400 µT)." @@ -948,7 +956,7 @@ mod test { PrivateKey::random(&mut OsRng), ) .with_change_script(script, ExecutionStack::default(), PrivateKey::default()); - let result = builder.build::(&factories).unwrap(); + let result = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); // Peek inside and check the results if let SenderState::Failed(TransactionProtocolError::UnsupportedError(s)) = result.into_state() { assert_eq!(s, "Multiple recipients are not supported yet") @@ -996,7 +1004,7 @@ mod test { PrivateKey::random(&mut OsRng), ) .with_change_script(script, ExecutionStack::default(), PrivateKey::default()); - let result = builder.build::(&factories).unwrap(); + let result = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); // Peek inside and check the results if let SenderState::SingleRoundMessageReady(info) = result.into_state() { assert_eq!(info.num_recipients, 1, "Number of receivers"); @@ -1047,7 +1055,7 @@ mod test { PrivateKey::random(&mut OsRng), ) .with_change_script(script, ExecutionStack::default(), PrivateKey::default()); - let result = builder.build::(&factories); + let result = builder.build::(&factories, None, Some(u64::MAX)); match result { Ok(_) => panic!("Range proof should have failed to verify"), diff --git a/base_layer/core/src/validation/block_validators/async_validator.rs b/base_layer/core/src/validation/block_validators/async_validator.rs index 4acab94d65..d8f5cb9dfe 100644 --- a/base_layer/core/src/validation/block_validators/async_validator.rs +++ b/base_layer/core/src/validation/block_validators/async_validator.rs @@ -41,9 +41,9 @@ use crate::{ use async_trait::async_trait; use futures::{stream::FuturesUnordered, StreamExt}; use log::*; -use std::{cmp, cmp::Ordering, thread, time::Instant}; +use std::{cmp, cmp::Ordering, convert::TryInto, thread, time::Instant}; use tari_common_types::types::{Commitment, HashOutput, PublicKey}; -use tari_crypto::commitment::HomomorphicCommitmentFactory; +use tari_crypto::{commitment::HomomorphicCommitmentFactory, script::ScriptContext}; use tokio::task; /// This validator checks whether a block satisfies consensus rules. @@ -229,12 +229,15 @@ impl BlockValidator { let block_height = header.height; let commitment_factory = self.factories.commitment.clone(); let db = self.db.inner().clone(); + let prev_hash: [u8; 32] = header.prev_hash.as_slice().try_into().unwrap_or([0; 32]); + let height = header.height; task::spawn_blocking(move || { let timer = Instant::now(); let mut aggregate_input_key = PublicKey::default(); let mut commitment_sum = Commitment::default(); let mut not_found_inputs = Vec::new(); let db = db.db_read_access()?; + for (i, input) in inputs.iter().enumerate() { // Check for duplicates and/or incorrect sorting if i > 0 && input <= &inputs[i - 1] { @@ -268,8 +271,10 @@ impl BlockValidator { // Once we've found unknown inputs, the aggregate data will be discarded and there is no reason to run // the tari script if not_found_inputs.is_empty() { + let context = ScriptContext::new(height, &prev_hash, &input.commitment); // lets count up the input script public keys - aggregate_input_key = aggregate_input_key + input.run_and_verify_script(&commitment_factory)?; + aggregate_input_key = + aggregate_input_key + input.run_and_verify_script(&commitment_factory, Some(context))?; commitment_sum = &commitment_sum + &input.commitment; } } diff --git a/base_layer/core/src/validation/helpers.rs b/base_layer/core/src/validation/helpers.rs index ef099efe4a..f8d54f88a6 100644 --- a/base_layer/core/src/validation/helpers.rs +++ b/base_layer/core/src/validation/helpers.rs @@ -233,6 +233,8 @@ pub fn check_accounting_balance( bypass_range_proof_verification, total_coinbase, factories, + Some(block.header.prev_hash.clone()), + Some(block.header.height), ) .map_err(|err| { warn!( diff --git a/base_layer/core/src/validation/transaction_validators.rs b/base_layer/core/src/validation/transaction_validators.rs index 169b29c923..0d5b23d80f 100644 --- a/base_layer/core/src/validation/transaction_validators.rs +++ b/base_layer/core/src/validation/transaction_validators.rs @@ -41,24 +41,35 @@ pub const LOG_TARGET: &str = "c::val::transaction_validators"; /// 1. Range proofs of the outputs are valid /// /// This function does NOT check that inputs come from the UTXO set -pub struct TxInternalConsistencyValidator { +pub struct TxInternalConsistencyValidator { + db: BlockchainDatabase, factories: CryptoFactories, bypass_range_proof_verification: bool, } -impl TxInternalConsistencyValidator { - pub fn new(factories: CryptoFactories, bypass_range_proof_verification: bool) -> Self { +impl TxInternalConsistencyValidator { + pub fn new(factories: CryptoFactories, bypass_range_proof_verification: bool, db: BlockchainDatabase) -> Self { Self { + db, factories, bypass_range_proof_verification, } } } -impl MempoolTransactionValidation for TxInternalConsistencyValidator { +impl MempoolTransactionValidation for TxInternalConsistencyValidator { fn validate(&self, tx: &Transaction) -> Result<(), ValidationError> { - tx.validate_internal_consistency(self.bypass_range_proof_verification, &self.factories, None) - .map_err(ValidationError::TransactionError)?; + let db = self.db.db_read_access()?; + let tip = db.fetch_chain_metadata()?; + + tx.validate_internal_consistency( + self.bypass_range_proof_verification, + &self.factories, + None, + Some(tip.best_block().clone()), + Some(tip.height_of_longest_chain()), + ) + .map_err(ValidationError::TransactionError)?; Ok(()) } } diff --git a/base_layer/core/tests/mempool.rs b/base_layer/core/tests/mempool.rs index eeed7ad193..444ac1f815 100644 --- a/base_layer/core/tests/mempool.rs +++ b/base_layer/core/tests/mempool.rs @@ -1066,7 +1066,9 @@ async fn consensus_validation_large_tx() { // make sure the tx was correctly made and is valid let factories = CryptoFactories::default(); - assert!(tx.validate_internal_consistency(true, &factories, None).is_ok()); + assert!(tx + .validate_internal_consistency(true, &factories, None, None, Some(u64::MAX)) + .is_ok()); let weighting = constants.transaction_weight(); let weight = tx.calculate_weight(weighting); diff --git a/base_layer/core/tests/node_comms_interface.rs b/base_layer/core/tests/node_comms_interface.rs index b5461f4f02..9e1bce86f1 100644 --- a/base_layer/core/tests/node_comms_interface.rs +++ b/base_layer/core/tests/node_comms_interface.rs @@ -354,6 +354,7 @@ async fn inbound_fetch_blocks_before_horizon_height() { key, PublicKey::from_secret_key(&offset), metadata_signature, + 0, ); let mut txn = DbTransaction::new(); txn.insert_utxo(utxo.clone(), block0.hash().clone(), 0, 4002); diff --git a/base_layer/wallet/migrations/2021-11-11-094000_add_script_lock_height/down.sql b/base_layer/wallet/migrations/2021-11-11-094000_add_script_lock_height/down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/base_layer/wallet/migrations/2021-11-11-094000_add_script_lock_height/up.sql b/base_layer/wallet/migrations/2021-11-11-094000_add_script_lock_height/up.sql new file mode 100644 index 0000000000..da064ece7d --- /dev/null +++ b/base_layer/wallet/migrations/2021-11-11-094000_add_script_lock_height/up.sql @@ -0,0 +1,31 @@ +-- Copyright 2021. The Tari Project +-- +-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +-- following conditions are met: +-- +-- 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +-- disclaimer. +-- +-- 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +-- following disclaimer in the documentation and/or other materials provided with the distribution. +-- +-- 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +-- products derived from this software without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +-- INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +-- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +-- WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +-- USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +ALTER TABLE outputs + ADD script_lock_height UNSIGNED BIGINT NOT NULL DEFAULT 0; + +ALTER TABLE outputs + ADD spending_priority UNSIGNED Integer NOT NULL DEFAULT 500; + +ALTER TABLE known_one_sided_payment_scripts + ADD script_lock_height UNSIGNED BIGINT NOT NULL DEFAULT 0; + diff --git a/base_layer/wallet/src/base_node_service/mock_base_node_service.rs b/base_layer/wallet/src/base_node_service/mock_base_node_service.rs index ff12309823..e7de7a9d6e 100644 --- a/base_layer/wallet/src/base_node_service/mock_base_node_service.rs +++ b/base_layer/wallet/src/base_node_service/mock_base_node_service.rs @@ -94,7 +94,7 @@ impl MockBaseNodeService { } pub fn set_default_base_node_state(&mut self) { - let metadata = ChainMetadata::new(u64::MAX, Vec::new(), 0, 0, 0); + let metadata = ChainMetadata::new(i64::MAX as u64, Vec::new(), 0, 0, 0); self.state = BaseNodeState { chain_metadata: Some(metadata), is_synced: Some(true), diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index 710f52307a..37afdbd641 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -23,7 +23,7 @@ use crate::output_manager_service::{ error::OutputManagerError, service::Balance, - storage::models::KnownOneSidedPaymentScript, + storage::models::{KnownOneSidedPaymentScript, SpendingPriority}, }; use aes_gcm::Aes256Gcm; use std::{fmt, sync::Arc}; @@ -46,9 +46,9 @@ use tower::Service; /// API Request enum pub enum OutputManagerRequest { GetBalance, - AddOutput(Box), - AddOutputWithTxId((TxId, Box)), - AddUnvalidatedOutput((TxId, Box)), + AddOutput((Box, Option)), + AddOutputWithTxId((TxId, Box, Option)), + AddUnvalidatedOutput((TxId, Box, Option)), UpdateOutputMetadataSignature(Box), GetRecipientTransaction(TransactionSenderMessage), GetCoinbaseTransaction((u64, MicroTari, MicroTari, u64)), @@ -78,6 +78,7 @@ pub enum OutputManagerRequest { ReinstateCancelledInboundTx(TxId), SetCoinbaseAbandoned(TxId, bool), CreateClaimShaAtomicSwapTransaction(HashOutput, PublicKey, MicroTari), + CreateHtlcRefundTransaction(HashOutput, MicroTari), } impl fmt::Display for OutputManagerRequest { @@ -85,8 +86,11 @@ impl fmt::Display for OutputManagerRequest { use OutputManagerRequest::*; match self { GetBalance => write!(f, "GetBalance"), - AddOutput(v) => write!(f, "AddOutput ({})", v.value), - AddOutputWithTxId((t, v)) => write!(f, "AddOutputWithTxId ({}: {})", t, v.value), + AddOutput((v, _)) => write!(f, "AddOutput ({})", v.value), + AddOutputWithTxId((t, v, _)) => write!(f, "AddOutputWithTxId ({}: {})", t, v.value), + AddUnvalidatedOutput((t, v, _)) => { + write!(f, "AddUnvalidatedOutput ({}: {})", t, v.value) + }, UpdateOutputMetadataSignature(v) => write!( f, "UpdateOutputMetadataSignature ({}, {}, {})", @@ -132,9 +136,12 @@ impl fmt::Display for OutputManagerRequest { pre_image, fee_per_gram, ), - OutputManagerRequest::AddUnvalidatedOutput((t, v)) => { - write!(f, "AddUnvalidatedOutput ({}: {})", t, v.value) - }, + CreateHtlcRefundTransaction(output, fee_per_gram) => write!( + f, + "CreateHtlcRefundTransaction(output hash: {}, , fee_per_gram: {} )", + output.to_hex(), + fee_per_gram, + ), } } } @@ -168,7 +175,7 @@ pub enum OutputManagerResponse { AddKnownOneSidedPaymentScript, ReinstatedCancelledInboundTx, CoinbaseAbandonedSet, - ClaimShaAtomicSwapTransaction((u64, MicroTari, MicroTari, Transaction)), + ClaimHtlcTransaction((u64, MicroTari, MicroTari, Transaction)), } pub type OutputManagerEventSender = broadcast::Sender>; @@ -212,10 +219,14 @@ impl OutputManagerHandle { self.event_stream_sender.subscribe() } - pub async fn add_output(&mut self, output: UnblindedOutput) -> Result<(), OutputManagerError> { + pub async fn add_output( + &mut self, + output: UnblindedOutput, + spend_priority: Option, + ) -> Result<(), OutputManagerError> { match self .handle - .call(OutputManagerRequest::AddOutput(Box::new(output))) + .call(OutputManagerRequest::AddOutput((Box::new(output), spend_priority))) .await?? { OutputManagerResponse::OutputAdded => Ok(()), @@ -227,10 +238,15 @@ impl OutputManagerHandle { &mut self, tx_id: TxId, output: UnblindedOutput, + spend_priority: Option, ) -> Result<(), OutputManagerError> { match self .handle - .call(OutputManagerRequest::AddOutputWithTxId((tx_id, Box::new(output)))) + .call(OutputManagerRequest::AddOutputWithTxId(( + tx_id, + Box::new(output), + spend_priority, + ))) .await?? { OutputManagerResponse::OutputAdded => Ok(()), @@ -242,10 +258,15 @@ impl OutputManagerHandle { &mut self, tx_id: TxId, output: UnblindedOutput, + spend_priority: Option, ) -> Result<(), OutputManagerError> { match self .handle - .call(OutputManagerRequest::AddUnvalidatedOutput((tx_id, Box::new(output)))) + .call(OutputManagerRequest::AddUnvalidatedOutput(( + tx_id, + Box::new(output), + spend_priority, + ))) .await?? { OutputManagerResponse::OutputAdded => Ok(()), @@ -456,6 +477,21 @@ impl OutputManagerHandle { } } + pub async fn create_htlc_refund_transaction( + &mut self, + output: HashOutput, + fee_per_gram: MicroTari, + ) -> Result<(u64, MicroTari, MicroTari, Transaction), OutputManagerError> { + match self + .handle + .call(OutputManagerRequest::CreateHtlcRefundTransaction(output, fee_per_gram)) + .await?? + { + OutputManagerResponse::ClaimHtlcTransaction(ct) => Ok(ct), + _ => Err(OutputManagerError::UnexpectedApiResponse), + } + } + pub async fn create_claim_sha_atomic_swap_transaction( &mut self, output: HashOutput, @@ -471,7 +507,7 @@ impl OutputManagerHandle { )) .await?? { - OutputManagerResponse::ClaimShaAtomicSwapTransaction(ct) => Ok(ct), + OutputManagerResponse::ClaimHtlcTransaction(ct) => Ok(ct), _ => Err(OutputManagerError::UnexpectedApiResponse), } } diff --git a/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs b/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs index 650a6f2bef..b7b35ae8f7 100644 --- a/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs +++ b/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs @@ -93,6 +93,7 @@ where TBackend: OutputManagerBackend + 'static ) }) }) + //Todo this needs some investigation. We assume Nop script here and recovery here might create an unspendable output if the script does not equal Nop. .map( |(output, features, script, sender_offset_public_key, metadata_signature)| { // Todo we need to look here that we might want to fail a specific output and not recover it as this @@ -108,6 +109,7 @@ where TBackend: OutputManagerBackend + 'static script_key, sender_offset_public_key, metadata_signature, + 0, ) }, ) @@ -117,7 +119,7 @@ where TBackend: OutputManagerBackend + 'static self.update_outputs_script_private_key_and_update_key_manager_index(output) .await?; - let db_output = DbUnblindedOutput::from_unblinded_output(output.clone(), &self.factories)?; + let db_output = DbUnblindedOutput::from_unblinded_output(output.clone(), &self.factories, None)?; let output_hex = db_output.commitment.to_hex(); if let Err(e) = self.db.add_unspent_output(db_output).await { match e { diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index e4738d8d09..2572402e69 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -31,7 +31,7 @@ use crate::{ resources::OutputManagerResources, storage::{ database::{OutputManagerBackend, OutputManagerDatabase}, - models::{DbUnblindedOutput, KnownOneSidedPaymentScript}, + models::{DbUnblindedOutput, KnownOneSidedPaymentScript, SpendingPriority}, }, tasks::TxoValidationTask, MasterKeyManager, @@ -43,7 +43,7 @@ use diesel::result::{DatabaseErrorKind, Error as DieselError}; use futures::{pin_mut, StreamExt}; use log::*; use rand::{rngs::OsRng, RngCore}; -use std::{cmp::Ordering, convert::TryInto, fmt, fmt::Display, sync::Arc}; +use std::{convert::TryInto, fmt, fmt::Display, sync::Arc}; use tari_common_types::{ transaction::TxId, types::{HashOutput, PrivateKey, PublicKey}, @@ -187,12 +187,16 @@ where ) -> Result { trace!(target: LOG_TARGET, "Handling Service Request: {}", request); match request { - OutputManagerRequest::AddOutput(uo) => self - .add_output(None, *uo) + OutputManagerRequest::AddOutput((uo, spend_priority)) => self + .add_output(None, *uo, spend_priority) .await .map(|_| OutputManagerResponse::OutputAdded), - OutputManagerRequest::AddOutputWithTxId((tx_id, uo)) => self - .add_output(Some(tx_id), *uo) + OutputManagerRequest::AddOutputWithTxId((tx_id, uo, spend_priority)) => self + .add_output(Some(tx_id), *uo, spend_priority) + .await + .map(|_| OutputManagerResponse::OutputAdded), + OutputManagerRequest::AddUnvalidatedOutput((tx_id, uo, spend_priority)) => self + .add_unvalidated_output(tx_id, *uo, spend_priority) .await .map(|_| OutputManagerResponse::OutputAdded), OutputManagerRequest::UpdateOutputMetadataSignature(uo) => self @@ -339,10 +343,10 @@ where self.claim_sha_atomic_swap_with_hash(output_hash, pre_image, fee_per_gram) .await }, - OutputManagerRequest::AddUnvalidatedOutput((tx_id, uo)) => self - .add_unvalidated_output(tx_id, *uo) + OutputManagerRequest::CreateHtlcRefundTransaction(output, fee_per_gram) => self + .create_htlc_refund_transaction(output, fee_per_gram) .await - .map(|_| OutputManagerResponse::OutputAdded), + .map(OutputManagerResponse::ClaimHtlcTransaction), } } @@ -360,7 +364,7 @@ where self.create_claim_sha_atomic_swap_transaction(output, pre_image, fee_per_gram) .await - .map(OutputManagerResponse::ClaimShaAtomicSwapTransaction) + .map(OutputManagerResponse::ClaimHtlcTransaction) } fn handle_base_node_service_event(&mut self, event: Arc) { @@ -422,12 +426,23 @@ where } /// Add an unblinded output to the outputs table and marks is as `Unspent`. - pub async fn add_output(&mut self, tx_id: Option, output: UnblindedOutput) -> Result<(), OutputManagerError> { + pub async fn add_output( + &mut self, + tx_id: Option, + output: UnblindedOutput, + spend_priority: Option, + ) -> Result<(), OutputManagerError> { debug!( target: LOG_TARGET, "Add output of value {} to Output Manager", output.value ); - let output = DbUnblindedOutput::from_unblinded_output(output, &self.resources.factories)?; + + let output = DbUnblindedOutput::from_unblinded_output(output, &self.resources.factories, spend_priority)?; + debug!( + target: LOG_TARGET, + "saving output of hash {} to Output Manager", + output.hash.to_hex() + ); match tx_id { None => self.resources.db.add_unspent_output(output).await?, Some(t) => self.resources.db.add_unspent_output_with_tx_id(t, output).await?, @@ -441,12 +456,13 @@ where &mut self, tx_id: TxId, output: UnblindedOutput, + spend_priority: Option, ) -> Result<(), OutputManagerError> { debug!( target: LOG_TARGET, "Add unvalidated output of value {} to Output Manager", output.value ); - let output = DbUnblindedOutput::from_unblinded_output(output, &self.resources.factories)?; + let output = DbUnblindedOutput::from_unblinded_output(output, &self.resources.factories, spend_priority)?; self.resources.db.add_unvalidated_output(tx_id, output).await?; Ok(()) } @@ -513,8 +529,10 @@ where &single_round_sender_data.sender_offset_public_key.clone(), &single_round_sender_data.public_commitment_nonce.clone(), )?, + 0, ), &self.resources.factories, + None, )?; self.resources @@ -648,7 +666,7 @@ where } let stp = builder - .build::(&self.resources.factories) + .build::(&self.resources.factories, None, self.last_seen_tip_height) .map_err(|e| OutputManagerError::BuildError(e.message))?; // If a change output was created add it to the pending_outputs list. @@ -662,6 +680,7 @@ where change_output.push(DbUnblindedOutput::from_unblinded_output( unblinded_output, &self.resources.factories, + None, )?); } @@ -710,7 +729,7 @@ where .with_rewind_data(self.resources.master_key_manager.rewind_data().clone()) .build_with_reward(&self.resources.consensus_constants, reward)?; - let output = DbUnblindedOutput::from_unblinded_output(unblinded_output, &self.resources.factories)?; + let output = DbUnblindedOutput::from_unblinded_output(unblinded_output, &self.resources.factories, None)?; // Clear any existing pending coinbase transactions for this blockheight if they exist if let Err(e) = self @@ -815,8 +834,10 @@ where script_private_key, PublicKey::from_secret_key(&sender_offset_private_key), metadata_signature, + 0, ), &self.resources.factories, + None, )?; builder .with_output(utxo.unblinded_output.clone(), sender_offset_private_key.clone()) @@ -841,7 +862,7 @@ where let factories = CryptoFactories::default(); let mut stp = builder - .build::(&self.resources.factories) + .build::(&self.resources.factories, None, self.last_seen_tip_height) .map_err(|e| OutputManagerError::BuildError(e.message))?; if input_selection.requires_change_output() { @@ -850,7 +871,8 @@ where "There should be a change output metadata signature available".to_string(), ) })?; - let change_output = DbUnblindedOutput::from_unblinded_output(unblinded_output, &self.resources.factories)?; + let change_output = + DbUnblindedOutput::from_unblinded_output(unblinded_output, &self.resources.factories, None)?; outputs.push(change_output); } @@ -866,7 +888,7 @@ where self.confirm_encumberance(tx_id).await?; let fee = stp.get_fee_amount()?; trace!(target: LOG_TARGET, "Finalize send-to-self transaction ({}).", tx_id); - stp.finalize(KernelFeatures::empty(), &factories)?; + stp.finalize(KernelFeatures::empty(), &factories, None, self.last_seen_tip_height)?; let tx = stp.take_transaction()?; Ok((fee, tx)) @@ -922,78 +944,40 @@ where let mut fee_with_change = MicroTari::from(0); let fee_calc = self.get_fee_calc(); - let uo = self.resources.db.fetch_sorted_unspent_outputs().await?; - // Attempt to get the chain tip height let chain_metadata = self.base_node_service.get_chain_metadata().await?; let (connected, tip_height) = match &chain_metadata { - Some(metadata) => (true, metadata.height_of_longest_chain()), - None => (false, 0), + Some(metadata) => (true, Some(metadata.height_of_longest_chain())), + None => (false, None), }; // If no strategy was specified and no metadata is available, then make sure to use MaturitythenSmallest let strategy = match (strategy, connected) { - (Some(s), _) => Some(s), - (None, false) => Some(UTXOSelectionStrategy::MaturityThenSmallest), - (None, true) => None, // use the selection heuristic next - }; - - // If we know the chain height then filter out unspendable UTXOs - let num_utxos = uo.len(); - let uo = if connected { - let mature_utxos = uo - .into_iter() - .filter(|u| u.unblinded_output.features.maturity <= tip_height) - .collect::>(); - - trace!( - target: LOG_TARGET, - "Some UTXOs have not matured yet at height {}, filtered {} UTXOs", - tip_height, - num_utxos - mature_utxos.len() - ); - - mature_utxos - } else { - uo + (Some(s), _) => s, + (None, false) => UTXOSelectionStrategy::MaturityThenSmallest, + (None, true) => UTXOSelectionStrategy::Default, // use the selection heuristic next }; // Heuristic for selection strategy: Default to MaturityThenSmallest, but if the amount is greater than // the largest UTXO, use Largest UTXOs first. - let strategy = match (strategy, uo.is_empty()) { - (Some(s), _) => s, - (None, true) => UTXOSelectionStrategy::Smallest, - (None, false) => { - let largest_utxo = &uo[uo.len() - 1]; - if amount > largest_utxo.unblinded_output.value { - UTXOSelectionStrategy::Largest - } else { - UTXOSelectionStrategy::MaturityThenSmallest - } - }, - }; + // let strategy = match (strategy, uo.is_empty()) { + // (Some(s), _) => s, + // (None, true) => UTXOSelectionStrategy::Smallest, + // (None, false) => { + // let largest_utxo = &uo[uo.len() - 1]; + // if amount > largest_utxo.unblinded_output.value { + // UTXOSelectionStrategy::Largest + // } else { + // UTXOSelectionStrategy::MaturityThenSmallest + // } + // }, + // }; debug!(target: LOG_TARGET, "select_utxos selection strategy: {}", strategy); - - let uo = match strategy { - UTXOSelectionStrategy::Smallest => uo, - UTXOSelectionStrategy::MaturityThenSmallest => { - let mut uo = uo; - uo.sort_by(|a, b| { - match a - .unblinded_output - .features - .maturity - .cmp(&b.unblinded_output.features.maturity) - { - Ordering::Equal => a.unblinded_output.value.cmp(&b.unblinded_output.value), - Ordering::Less => Ordering::Less, - Ordering::Greater => Ordering::Greater, - } - }); - uo - }, - UTXOSelectionStrategy::Largest => uo.into_iter().rev().collect(), - }; + let uo = self + .resources + .db + .fetch_unspent_outputs_for_spending(strategy, amount, tip_height) + .await?; trace!(target: LOG_TARGET, "We found {} UTXOs to select from", uo.len()); // Assumes that default Outputfeatures are used for change utxo @@ -1143,8 +1127,10 @@ where script_private_key, sender_offset_public_key, metadata_signature, + 0, ), &self.resources.factories, + None, )?; builder .with_output(utxo.unblinded_output.clone(), sender_offset_private_key) @@ -1169,7 +1155,7 @@ where let factories = CryptoFactories::default(); let mut stp = builder - .build::(&self.resources.factories) + .build::(&self.resources.factories, None, self.last_seen_tip_height) .map_err(|e| OutputManagerError::BuildError(e.message))?; // The Transaction Protocol built successfully so we will pull the unspent outputs out of the unspent list and // store them until the transaction times out OR is confirmed @@ -1189,6 +1175,7 @@ where outputs.push(DbUnblindedOutput::from_unblinded_output( unblinded_output, &self.resources.factories, + None, )?); } @@ -1198,7 +1185,7 @@ where .await?; self.confirm_encumberance(tx_id).await?; trace!(target: LOG_TARGET, "Finalize coin split transaction ({}).", tx_id); - stp.finalize(KernelFeatures::empty(), &factories)?; + stp.finalize(KernelFeatures::empty(), &factories, None, self.last_seen_tip_height)?; let fee = stp.get_fee_amount()?; let tx = stp.take_transaction()?; Ok((tx_id, tx, fee, utxos_total_value)) @@ -1257,6 +1244,9 @@ where self.node_identity.as_ref().secret_key().clone(), output.sender_offset_public_key, output.metadata_signature, + // Although the technically the script does have a script lock higher than 0, this does not apply to to us + // as we are claiming the Hashed part which has a 0 time lock + 0, ); let amount = rewound.committed_value; @@ -1295,7 +1285,7 @@ where let factories = CryptoFactories::default(); let mut stp = builder - .build::(&self.resources.factories) + .build::(&self.resources.factories, None, self.last_seen_tip_height) .map_err(|e| OutputManagerError::BuildError(e.message))?; let tx_id = stp.get_tx_id()?; @@ -1303,7 +1293,8 @@ where let unblinded_output = stp.get_change_unblinded_output()?.ok_or_else(|| { OutputManagerError::BuildError("There should be a change output metadata signature available".to_string()) })?; - let change_output = DbUnblindedOutput::from_unblinded_output(unblinded_output, &self.resources.factories)?; + let change_output = + DbUnblindedOutput::from_unblinded_output(unblinded_output, &self.resources.factories, None)?; outputs.push(change_output); trace!(target: LOG_TARGET, "Claiming HTLC with transaction ({}).", tx_id); @@ -1311,12 +1302,88 @@ where self.confirm_encumberance(tx_id).await?; let fee = stp.get_fee_amount()?; trace!(target: LOG_TARGET, "Finalize send-to-self transaction ({}).", tx_id); - stp.finalize(KernelFeatures::empty(), &factories)?; + stp.finalize(KernelFeatures::empty(), &factories, None, self.last_seen_tip_height)?; let tx = stp.take_transaction()?; Ok((tx_id, fee, amount - fee, tx)) } + pub async fn create_htlc_refund_transaction( + &mut self, + output_hash: HashOutput, + fee_per_gram: MicroTari, + ) -> Result<(u64, MicroTari, MicroTari, Transaction), OutputManagerError> { + let output = self + .resources + .db + .get_unspent_output(output_hash) + .await? + .unblinded_output; + + let amount = output.value; + + let offset = PrivateKey::random(&mut OsRng); + let nonce = PrivateKey::random(&mut OsRng); + let message = "SHA-XTR atomic refund".to_string(); + + // Create builder with no recipients (other than ourselves) + let mut builder = SenderTransactionProtocol::builder(0, self.resources.consensus_constants.clone()); + builder + .with_lock_height(0) + .with_fee_per_gram(fee_per_gram) + .with_offset(offset.clone()) + .with_private_nonce(nonce.clone()) + .with_message(message) + .with_prevent_fee_gt_amount(self.resources.config.prevent_fee_gt_amount) + .with_input( + output.as_transaction_input(&self.resources.factories.commitment)?, + output, + ); + + let mut outputs = Vec::new(); + + let (spending_key, script_private_key) = self + .resources + .master_key_manager + .get_next_spend_and_script_key() + .await?; + builder.with_change_secret(spending_key); + builder.with_rewindable_outputs(self.resources.master_key_manager.rewind_data().clone()); + builder.with_change_script( + script!(Nop), + inputs!(PublicKey::from_secret_key(&script_private_key)), + script_private_key, + ); + + let factories = CryptoFactories::default(); + println!("he`"); + let mut stp = builder + .build::(&self.resources.factories, None, self.last_seen_tip_height) + .map_err(|e| OutputManagerError::BuildError(e.message))?; + + let tx_id = stp.get_tx_id()?; + + let unblinded_output = stp.get_change_unblinded_output()?.ok_or_else(|| { + OutputManagerError::BuildError("There should be a change output metadata signature available".to_string()) + })?; + + let change_output = + DbUnblindedOutput::from_unblinded_output(unblinded_output, &self.resources.factories, None)?; + outputs.push(change_output); + + trace!(target: LOG_TARGET, "Claiming HTLC refund with transaction ({}).", tx_id); + + let fee = stp.get_fee_amount()?; + + stp.finalize(KernelFeatures::empty(), &factories, None, self.last_seen_tip_height)?; + + let tx = stp.take_transaction()?; + + self.resources.db.encumber_outputs(tx_id, Vec::new(), outputs).await?; + self.confirm_encumberance(tx_id).await?; + Ok((tx_id, fee, amount - fee, tx)) + } + /// Persist a one-sided payment script for a Comms Public/Private key. These are the scripts that this wallet knows /// to look for when scanning for one-sided payments async fn add_known_script(&mut self, known_script: KnownOneSidedPaymentScript) -> Result<(), OutputManagerError> { @@ -1370,9 +1437,13 @@ where known_one_sided_payment_scripts[i].private_key.clone(), output.sender_offset_public_key, output.metadata_signature, + known_one_sided_payment_scripts[i].script_lock_height, ); - let db_output = - DbUnblindedOutput::from_unblinded_output(rewound_output.clone(), &self.resources.factories)?; + let db_output = DbUnblindedOutput::from_unblinded_output( + rewound_output.clone(), + &self.resources.factories, + None, + )?; let output_hex = output.commitment.to_hex(); match self.resources.db.add_unspent_output(db_output).await { @@ -1410,7 +1481,7 @@ where /// Different UTXO selection strategies for choosing which UTXO's are used to fulfill a transaction /// TODO Investigate and implement more optimal strategies -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum UTXOSelectionStrategy { // Start from the smallest UTXOs and work your way up until the amount is covered. Main benefit // is removing small UTXOs from the blockchain, con is that it costs more in fees @@ -1419,6 +1490,9 @@ pub enum UTXOSelectionStrategy { MaturityThenSmallest, // A strategy that selects the largest UTXOs first. Preferred when the amount is large Largest, + // Heuristic for selection strategy: MaturityThenSmallest, but if the amount is greater than + // the largest UTXO, use Largest UTXOs first + Default, } impl Display for UTXOSelectionStrategy { @@ -1427,6 +1501,7 @@ impl Display for UTXOSelectionStrategy { UTXOSelectionStrategy::Smallest => write!(f, "Smallest"), UTXOSelectionStrategy::MaturityThenSmallest => write!(f, "MaturityThenSmallest"), UTXOSelectionStrategy::Largest => write!(f, "Largest"), + UTXOSelectionStrategy::Default => write!(f, "Default"), } } } diff --git a/base_layer/wallet/src/output_manager_service/storage/database.rs b/base_layer/wallet/src/output_manager_service/storage/database.rs index 72caec2822..78c40bfb40 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database.rs @@ -25,6 +25,9 @@ use crate::output_manager_service::{ service::Balance, storage::models::{DbUnblindedOutput, KnownOneSidedPaymentScript, OutputStatus}, }; +use tari_crypto::tari_utilities::hex::Hex; + +use crate::output_manager_service::service::UTXOSelectionStrategy; use aes_gcm::Aes256Gcm; use log::*; use std::{ @@ -35,7 +38,7 @@ use tari_common_types::{ transaction::TxId, types::{BlindingFactor, Commitment, HashOutput}, }; -use tari_core::transactions::transaction::TransactionOutput; +use tari_core::transactions::{tari_amount::MicroTari, transaction::TransactionOutput}; use tari_key_manager::cipher_seed::CipherSeed; const LOG_TARGET: &str = "wallet::output_manager_service::database"; @@ -130,6 +133,13 @@ pub trait OutputManagerBackend: Send + Sync + Clone { ) -> Result; /// Import unvalidated output fn add_unvalidated_output(&self, output: DbUnblindedOutput, tx_id: TxId) -> Result<(), OutputManagerStorageError>; + + fn fetch_unspent_outputs_for_spending( + &self, + strategy: UTXOSelectionStrategy, + amount: u64, + current_tip_height: Option, + ) -> Result, OutputManagerStorageError>; } /// Holds the state of the KeyManager being used by the Output Manager Service @@ -144,6 +154,7 @@ pub struct KeyManagerState { pub enum DbKey { SpentOutput(BlindingFactor), UnspentOutput(BlindingFactor), + UnspentOutputHash(HashOutput), AnyOutputByCommitment(Commitment), TimeLockedUnspentOutputs(u64), UnspentOutputs, @@ -387,6 +398,22 @@ where T: OutputManagerBackend + 'static Ok(uo) } + /// Retrieves UTXOs than can be spent, sorted by priority, then value from smallest to largest. + pub async fn fetch_unspent_outputs_for_spending( + &self, + strategy: UTXOSelectionStrategy, + amount: MicroTari, + tip_height: Option, + ) -> Result, OutputManagerStorageError> { + let db_clone = self.db.clone(); + let utxos = tokio::task::spawn_blocking(move || { + db_clone.fetch_unspent_outputs_for_spending(strategy, amount.as_u64(), tip_height) + }) + .await + .map_err(|err| OutputManagerStorageError::BlockingTaskSpawnError(err.to_string()))??; + Ok(utxos) + } + pub async fn fetch_spent_outputs(&self) -> Result, OutputManagerStorageError> { let db_clone = self.db.clone(); @@ -516,6 +543,27 @@ where T: OutputManagerBackend + 'static Ok(scripts) } + pub async fn get_unspent_output(&self, output: HashOutput) -> Result { + let db_clone = self.db.clone(); + + let uo = tokio::task::spawn_blocking( + move || match db_clone.fetch(&DbKey::UnspentOutputHash(output.clone())) { + Ok(None) => log_error( + DbKey::UnspentOutputHash(output.clone()), + OutputManagerStorageError::UnexpectedResult( + "Could not retrieve unspent output: ".to_string() + &output.to_hex(), + ), + ), + Ok(Some(DbValue::UnspentOutput(uo))) => Ok(uo), + Ok(Some(other)) => unexpected_result(DbKey::UnspentOutputHash(output), other), + Err(e) => log_error(DbKey::UnspentOutputHash(output), e), + }, + ) + .await + .map_err(|err| OutputManagerStorageError::BlockingTaskSpawnError(err.to_string()))??; + Ok(*uo) + } + pub async fn get_last_mined_output(&self) -> Result, OutputManagerStorageError> { self.db.get_last_mined_output() } @@ -630,6 +678,7 @@ impl Display for DbKey { match self { DbKey::SpentOutput(_) => f.write_str(&"Spent Output Key".to_string()), DbKey::UnspentOutput(_) => f.write_str(&"Unspent Output Key".to_string()), + DbKey::UnspentOutputHash(_) => f.write_str(&"Unspent Output Hash Key".to_string()), DbKey::UnspentOutputs => f.write_str(&"Unspent Outputs Key".to_string()), DbKey::SpentOutputs => f.write_str(&"Spent Outputs Key".to_string()), DbKey::KeyManagerState => f.write_str(&"Key Manager State".to_string()), diff --git a/base_layer/wallet/src/output_manager_service/storage/models.rs b/base_layer/wallet/src/output_manager_service/storage/models.rs index 198ad10054..7f0a8805c9 100644 --- a/base_layer/wallet/src/output_manager_service/storage/models.rs +++ b/base_layer/wallet/src/output_manager_service/storage/models.rs @@ -39,12 +39,14 @@ pub struct DbUnblindedOutput { pub mined_mmr_position: Option, pub marked_deleted_at_height: Option, pub marked_deleted_in_block: Option, + pub spend_priority: SpendingPriority, } impl DbUnblindedOutput { pub fn from_unblinded_output( output: UnblindedOutput, factory: &CryptoFactories, + spend_priority: Option, ) -> Result { let tx_out = output.as_transaction_output(factory)?; Ok(DbUnblindedOutput { @@ -56,6 +58,7 @@ impl DbUnblindedOutput { mined_mmr_position: None, marked_deleted_at_height: None, marked_deleted_in_block: None, + spend_priority: spend_priority.unwrap_or(SpendingPriority::Normal), }) } @@ -63,6 +66,7 @@ impl DbUnblindedOutput { output: UnblindedOutput, factory: &CryptoFactories, rewind_data: &RewindData, + spend_priority: Option, ) -> Result { let tx_out = output.as_rewindable_transaction_output(factory, rewind_data)?; Ok(DbUnblindedOutput { @@ -74,6 +78,7 @@ impl DbUnblindedOutput { mined_mmr_position: None, marked_deleted_at_height: None, marked_deleted_in_block: None, + spend_priority: spend_priority.unwrap_or(SpendingPriority::Normal), }) } } @@ -104,12 +109,39 @@ impl Ord for DbUnblindedOutput { impl Eq for DbUnblindedOutput {} +#[derive(Debug, Clone)] +pub enum SpendingPriority { + Normal, + HtlcSpendAsap, + Unknown, +} + +impl From for SpendingPriority { + fn from(value: u32) -> Self { + match value { + 100 => SpendingPriority::HtlcSpendAsap, + 500 => SpendingPriority::Normal, + _ => SpendingPriority::Unknown, + } + } +} + +impl From for u32 { + fn from(value: SpendingPriority) -> Self { + match value { + SpendingPriority::HtlcSpendAsap => 100, + SpendingPriority::Normal | SpendingPriority::Unknown => 500, + } + } +} + #[derive(Debug, Clone)] pub struct KnownOneSidedPaymentScript { pub script_hash: Vec, pub private_key: PrivateKey, pub script: TariScript, pub input: ExecutionStack, + pub script_lock_height: u64, } impl PartialEq for KnownOneSidedPaymentScript { diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs index 974c4a57fc..78a7fcddaf 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs @@ -51,7 +51,7 @@ use tari_key_manager::cipher_seed::CipherSeed; use crate::{ output_manager_service::{ error::OutputManagerStorageError, - service::Balance, + service::{Balance, UTXOSelectionStrategy}, storage::{ database::{DbKey, DbKeyValuePair, DbValue, KeyManagerState, OutputManagerBackend, WriteOperation}, models::{DbUnblindedOutput, KnownOneSidedPaymentScript, OutputStatus}, @@ -183,6 +183,19 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { None }, }, + DbKey::UnspentOutputHash(hash) => match OutputSql::find_by_hash(hash, OutputStatus::Unspent, &(*conn)) { + Ok(mut o) => { + self.decrypt_if_necessary(&mut o)?; + Some(DbValue::UnspentOutput(Box::new(DbUnblindedOutput::try_from(o)?))) + }, + Err(e) => { + match e { + OutputManagerStorageError::DieselError(DieselError::NotFound) => (), + e => return Err(e), + }; + None + }, + }, DbKey::AnyOutputByCommitment(commitment) => { match OutputSql::find_by_commitment(&commitment.to_vec(), &conn) { Ok(mut o) => { @@ -386,6 +399,7 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { } }, DbKey::SpentOutput(_s) => return Err(OutputManagerStorageError::OperationNotSupported), + DbKey::UnspentOutputHash(_h) => return Err(OutputManagerStorageError::OperationNotSupported), DbKey::UnspentOutput(_k) => return Err(OutputManagerStorageError::OperationNotSupported), DbKey::UnspentOutputs => return Err(OutputManagerStorageError::OperationNotSupported), DbKey::SpentOutputs => return Err(OutputManagerStorageError::OperationNotSupported), @@ -1190,6 +1204,37 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { } Ok(()) } + + /// Retrieves UTXOs than can be spent, sorted by priority, then value from smallest to largest. + fn fetch_unspent_outputs_for_spending( + &self, + strategy: UTXOSelectionStrategy, + amount: u64, + tip_height: Option, + ) -> Result, OutputManagerStorageError> { + let start = Instant::now(); + let conn = self.database_connection.get_pooled_connection()?; + let acquire_lock = start.elapsed(); + let tip = match tip_height { + Some(v) => v as i64, + None => i64::MAX, + }; + let mut outputs = OutputSql::fetch_unspent_outputs_for_spending(strategy, amount, tip, &conn)?; + for o in outputs.iter_mut() { + self.decrypt_if_necessary(o)?; + } + trace!( + target: LOG_TARGET, + "sqlite profile - fetch_unspent_outputs_for_spending: lock {} + db_op {} = {} ms", + acquire_lock.as_millis(), + (start.elapsed() - acquire_lock).as_millis(), + start.elapsed().as_millis() + ); + outputs + .iter() + .map(|o| DbUnblindedOutput::try_from(o.clone())) + .collect::, _>>() + } } impl TryFrom for OutputStatus { @@ -1436,6 +1481,7 @@ pub struct KnownOneSidedPaymentScriptSql { pub private_key: Vec, pub script: Vec, pub input: Vec, + pub script_lock_height: i64, } /// These are the fields that can be updated for an Output @@ -1536,11 +1582,13 @@ impl TryFrom for KnownOneSidedPaymentScript { error!(target: LOG_TARGET, "Could not create execution stack from stored bytes"); OutputManagerStorageError::ConversionError })?; + let script_lock_height = o.script_lock_height as u64; Ok(KnownOneSidedPaymentScript { script_hash, private_key, script, input, + script_lock_height, }) } } @@ -1548,6 +1596,7 @@ impl TryFrom for KnownOneSidedPaymentScript { /// Conversion from an KnownOneSidedPaymentScriptSQL to the datatype form impl From for KnownOneSidedPaymentScriptSql { fn from(known_script: KnownOneSidedPaymentScript) -> Self { + let script_lock_height = known_script.script_lock_height as i64; let script_hash = known_script.script_hash; let private_key = known_script.private_key.as_bytes().to_vec(); let script = known_script.script.as_bytes().to_vec(); @@ -1557,6 +1606,7 @@ impl From for KnownOneSidedPaymentScriptSql { private_key, script, input, + script_lock_height, } } } @@ -1644,7 +1694,7 @@ mod test { for _i in 0..2 { let (_, uo) = make_input(MicroTari::from(100 + OsRng.next_u64() % 1000)); - let uo = DbUnblindedOutput::from_unblinded_output(uo, &factories).unwrap(); + let uo = DbUnblindedOutput::from_unblinded_output(uo, &factories, None).unwrap(); let o = NewOutputSql::new(uo, OutputStatus::Unspent, None, None).unwrap(); outputs.push(o.clone()); outputs_unspent.push(o.clone()); @@ -1653,7 +1703,7 @@ mod test { for _i in 0..3 { let (_, uo) = make_input(MicroTari::from(100 + OsRng.next_u64() % 1000)); - let uo = DbUnblindedOutput::from_unblinded_output(uo, &factories).unwrap(); + let uo = DbUnblindedOutput::from_unblinded_output(uo, &factories, None).unwrap(); let o = NewOutputSql::new(uo, OutputStatus::Spent, None, None).unwrap(); outputs.push(o.clone()); outputs_spent.push(o.clone()); @@ -1768,7 +1818,7 @@ mod test { let factories = CryptoFactories::default(); let (_, uo) = make_input(MicroTari::from(100 + OsRng.next_u64() % 1000)); - let uo = DbUnblindedOutput::from_unblinded_output(uo, &factories).unwrap(); + let uo = DbUnblindedOutput::from_unblinded_output(uo, &factories, None).unwrap(); let output = NewOutputSql::new(uo, OutputStatus::Unspent, None, None).unwrap(); let key = GenericArray::from_slice(b"an example very very secret key."); @@ -1886,12 +1936,12 @@ mod test { let _state_sql = NewKeyManagerStateSql::from(starting_state).commit(&conn).unwrap(); let (_, uo) = make_input(MicroTari::from(100 + OsRng.next_u64() % 1000)); - let uo = DbUnblindedOutput::from_unblinded_output(uo, &factories).unwrap(); + let uo = DbUnblindedOutput::from_unblinded_output(uo, &factories, None).unwrap(); let output = NewOutputSql::new(uo, OutputStatus::Unspent, None, None).unwrap(); output.commit(&conn).unwrap(); let (_, uo2) = make_input(MicroTari::from(100 + OsRng.next_u64() % 1000)); - let uo2 = DbUnblindedOutput::from_unblinded_output(uo2, &factories).unwrap(); + let uo2 = DbUnblindedOutput::from_unblinded_output(uo2, &factories, None).unwrap(); let output2 = NewOutputSql::new(uo2, OutputStatus::Unspent, None, None).unwrap(); output2.commit(&conn).unwrap(); } diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs index c1bb5925cb..f02317119a 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs @@ -37,6 +37,7 @@ use crate::{ }; use aes_gcm::Aes256Gcm; +use crate::output_manager_service::service::UTXOSelectionStrategy; use diesel::{prelude::*, sql_query, SqliteConnection}; use log::*; use std::convert::TryFrom; @@ -86,6 +87,8 @@ pub struct OutputSql { pub received_in_tx_id: Option, pub spent_in_tx_id: Option, pub coinbase_block_height: Option, + pub script_lock_height: i64, + pub spend_priority: i32, } impl OutputSql { @@ -102,6 +105,55 @@ impl OutputSql { Ok(outputs::table.filter(outputs::status.eq(status as i32)).load(conn)?) } + /// Retrieves UTXOs than can be spent, sorted by priority, then value from smallest to largest. + pub fn fetch_unspent_outputs_for_spending( + mut strategy: UTXOSelectionStrategy, + amount: u64, + tip_height: i64, + conn: &SqliteConnection, + ) -> Result, OutputManagerStorageError> { + if strategy == UTXOSelectionStrategy::Default { + // lets get the max value for all utxos + let max: Vec = outputs::table + .filter(outputs::status.eq(OutputStatus::Unspent as i32)) + .filter(outputs::script_lock_height.le(tip_height)) + .filter(outputs::maturity.le(tip_height)) + .order(outputs::value.desc()) + .select(outputs::value) + .limit(1) + .load(conn)?; + if max.is_empty() { + strategy = UTXOSelectionStrategy::Smallest + } else if amount > max[0] as u64 { + strategy = UTXOSelectionStrategy::Largest + } else { + strategy = UTXOSelectionStrategy::MaturityThenSmallest + } + } + + let mut query = outputs::table + .into_boxed() + .filter(outputs::status.eq(OutputStatus::Unspent as i32)) + .filter(outputs::script_lock_height.le(tip_height)) + .filter(outputs::maturity.le(tip_height)) + .order_by(outputs::spending_priority.asc()); + match strategy { + UTXOSelectionStrategy::Smallest => { + query = query.then_order_by(outputs::value.asc()); + }, + UTXOSelectionStrategy::MaturityThenSmallest => { + query = query + .then_order_by(outputs::maturity.asc()) + .then_order_by(outputs::value.asc()); + }, + UTXOSelectionStrategy::Largest => { + query = query.then_order_by(outputs::value.desc()); + }, + UTXOSelectionStrategy::Default => {}, + }; + Ok(query.load(conn)?) + } + /// Return all unspent outputs that have a maturity above the provided chain tip pub fn index_time_locked(tip: u64, conn: &SqliteConnection) -> Result, OutputManagerStorageError> { Ok(outputs::table @@ -176,7 +228,7 @@ impl OutputSql { FROM outputs WHERE status = ? \ UNION ALL \ SELECT coalesce(sum(value), 0) as amount, 'time_locked_balance' as category \ - FROM outputs WHERE status = ? AND maturity > ? \ + FROM outputs WHERE status = ? AND maturity > ? OR script_lock_height > ? \ UNION ALL \ SELECT coalesce(sum(value), 0) as amount, 'pending_incoming_balance' as category \ FROM outputs WHERE status = ? OR status = ? OR status = ? \ @@ -189,6 +241,7 @@ impl OutputSql { // time_locked_balance .bind::(OutputStatus::Unspent as i32) .bind::(current_tip as i64) + .bind::(current_tip as i64) // pending_incoming_balance .bind::(OutputStatus::EncumberedToBeReceived as i32) .bind::(OutputStatus::ShortTermEncumberedToBeReceived as i32) @@ -333,6 +386,18 @@ impl OutputSql { .first::(conn)?) } + /// Find a particular Output, if it exists and is in the specified Spent state + pub fn find_by_hash( + hash: &[u8], + status: OutputStatus, + conn: &SqliteConnection, + ) -> Result { + Ok(outputs::table + .filter(outputs::status.eq(status as i32)) + .filter(outputs::hash.eq(Some(hash))) + .first::(conn)?) + } + /// Find a particular Output, if it exists and is in the specified Spent state pub fn find_pending_coinbase_at_block_height( block_height: u64, @@ -439,6 +504,7 @@ impl TryFrom for DbUnblindedOutput { OutputManagerStorageError::ConversionError })?, ), + o.script_lock_height as u64, ); let hash = match o.hash { @@ -457,7 +523,7 @@ impl TryFrom for DbUnblindedOutput { }, Some(c) => Commitment::from_vec(&c)?, }; - + let spend_priority = (o.spend_priority as u32).into(); Ok(Self { commitment, unblinded_output, @@ -467,6 +533,7 @@ impl TryFrom for DbUnblindedOutput { mined_mmr_position: o.mined_mmr_position.map(|mp| mp as u64), marked_deleted_at_height: o.marked_deleted_at_height.map(|d| d as u64), marked_deleted_in_block: o.marked_deleted_in_block, + spend_priority, }) } } diff --git a/base_layer/wallet/src/schema.rs b/base_layer/wallet/src/schema.rs index 570ae8cae1..383dc93f7b 100644 --- a/base_layer/wallet/src/schema.rs +++ b/base_layer/wallet/src/schema.rs @@ -68,6 +68,7 @@ table! { private_key -> Binary, script -> Binary, input -> Binary, + script_lock_height -> BigInt, } } @@ -112,6 +113,8 @@ table! { received_in_tx_id -> Nullable, spent_in_tx_id -> Nullable, coinbase_block_height -> Nullable, + script_lock_height -> BigInt, + spending_priority -> Integer, } } diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs index ca75e3145c..c02f61e2a2 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs @@ -40,6 +40,7 @@ use tari_comms::types::CommsPublicKey; use tokio::sync::{mpsc, oneshot}; use crate::connectivity_service::WalletConnectivityInterface; +use tari_common_types::types::HashOutput; use tari_core::transactions::{ transaction::Transaction, transaction_protocol::{recipient::RecipientState, sender::TransactionSenderMessage}, @@ -64,6 +65,8 @@ pub struct TransactionReceiveProtocol { resources: TransactionServiceResources, transaction_finalize_receiver: Option>, cancellation_receiver: Option>, + prev_header: Option, + height: Option, } impl TransactionReceiveProtocol @@ -79,6 +82,8 @@ where resources: TransactionServiceResources, transaction_finalize_receiver: mpsc::Receiver<(CommsPublicKey, TxId, Transaction)>, cancellation_receiver: oneshot::Receiver<()>, + prev_header: Option, + height: Option, ) -> Self { Self { id, @@ -88,6 +93,8 @@ where resources, transaction_finalize_receiver: Some(transaction_finalize_receiver), cancellation_receiver: Some(cancellation_receiver), + prev_header, + height, } } @@ -361,7 +368,13 @@ where ); finalized_transaction - .validate_internal_consistency(true, &self.resources.factories, None) + .validate_internal_consistency( + true, + &self.resources.factories, + None, + self.prev_header.clone(), + self.height, + ) .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; // Find your own output in the transaction diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs index 774351782e..13d2cd3d35 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs @@ -43,7 +43,10 @@ use chrono::Utc; use futures::FutureExt; use log::*; use std::sync::Arc; -use tari_common_types::transaction::{TransactionDirection, TransactionStatus}; +use tari_common_types::{ + transaction::{TransactionDirection, TransactionStatus}, + types::HashOutput, +}; use tari_comms::{peer_manager::NodeId, types::CommsPublicKey}; use tari_comms_dht::{ domain_message::OutboundDomainMessage, @@ -82,6 +85,8 @@ pub struct TransactionSendProtocol { resources: TransactionServiceResources, transaction_reply_receiver: Option>, cancellation_receiver: Option>, + prev_header: Option, + height: Option, } #[allow(clippy::too_many_arguments)] @@ -103,6 +108,8 @@ where oneshot::Sender>, >, stage: TransactionSendProtocolStage, + prev_header: Option, + height: Option, ) -> Self { Self { id, @@ -115,6 +122,8 @@ where message, service_request_reply_channel, stage, + prev_header, + height, } } @@ -452,7 +461,12 @@ where outbound_tx .sender_protocol - .finalize(KernelFeatures::empty(), &self.resources.factories) + .finalize( + KernelFeatures::empty(), + &self.resources.factories, + self.prev_header.clone(), + self.height, + ) .map_err(|e| { error!( target: LOG_TARGET, diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 076c9bc489..44d162a0fb 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -23,7 +23,7 @@ use crate::{ base_node_service::handle::{BaseNodeEvent, BaseNodeServiceHandle}, connectivity_service::WalletConnectivityInterface, - output_manager_service::handle::OutputManagerHandle, + output_manager_service::{handle::OutputManagerHandle, storage::models::SpendingPriority}, storage::database::{WalletBackend, WalletDatabase}, transaction_service::{ config::TransactionServiceConfig, @@ -50,7 +50,6 @@ use crate::{ util::watch::Watch, utxo_scanner_service::utxo_scanning::RECOVERY_KEY, }; - use chrono::{NaiveDateTime, Utc}; use digest::Digest; use futures::{pin_mut, stream::FuturesUnordered, Stream, StreamExt}; @@ -74,7 +73,7 @@ use tari_core::{ proto::base_node as base_node_proto, transactions::{ tari_amount::MicroTari, - transaction::{KernelFeatures, OutputFeatures, Transaction, TransactionOutput}, + transaction::{KernelFeatures, OutputFeatures, Transaction, TransactionOutput, UnblindedOutput}, transaction_protocol::{ proto, recipient::RecipientSignedMessage, @@ -86,6 +85,7 @@ use tari_core::{ }, }; use tari_crypto::{ + inputs, keys::{DiffieHellmanSharedSecret, PublicKey as PKtrait}, script, tari_utilities::ByteArray, @@ -791,6 +791,8 @@ where message, Some(reply_channel), TransactionSendProtocolStage::Initial, + None, + self.last_seen_tip_height, ); let join_handle = tokio::spawn(protocol.execute()); @@ -851,13 +853,13 @@ where .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; // TODO: Add a standardized Diffie-Hellman method to the tari_crypto library that will return a private key, // TODO: then come back and use it here. - let spending_key = PrivateKey::from_bytes( + let spend_key = PrivateKey::from_bytes( CommsPublicKey::shared_secret(&sender_offset_private_key.clone(), &dest_pubkey.clone()).as_bytes(), ) .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; let sender_message = TransactionSenderMessage::new_single_round_message(stp.get_single_round_message()?); - let rewind_key = PrivateKey::from_bytes(&hash_secret_key(&spending_key))?; + let rewind_key = PrivateKey::from_bytes(&hash_secret_key(&spend_key))?; let blinding_key = PrivateKey::from_bytes(&hash_secret_key(&rewind_key))?; let rewind_data = RewindData { rewind_key: rewind_key.clone(), @@ -868,7 +870,7 @@ where let rtp = ReceiverTransactionProtocol::new_with_rewindable_output( sender_message, PrivateKey::random(&mut OsRng), - spending_key, + spend_key.clone(), OutputFeatures::default(), &self.resources.factories, &rewind_data, @@ -876,6 +878,17 @@ where let recipient_reply = rtp.get_signed_data()?.clone(); let output = recipient_reply.output.clone(); + let unblinded_output = UnblindedOutput::new( + amount, + spend_key, + OutputFeatures::default(), + script, + inputs!(PublicKey::from_secret_key(self.node_identity.secret_key())), + self.node_identity.secret_key().clone(), + output.sender_offset_public_key.clone(), + output.metadata_signature.clone(), + height, + ); // Start finalizing @@ -884,14 +897,19 @@ where // Finalize - stp.finalize(KernelFeatures::empty(), &self.resources.factories) - .map_err(|e| { - error!( - target: LOG_TARGET, - "Transaction (TxId: {}) could not be finalized. Failure error: {:?}", tx_id, e, - ); - TransactionServiceProtocolError::new(tx_id, e.into()) - })?; + stp.finalize( + KernelFeatures::empty(), + &self.resources.factories, + None, + self.last_seen_tip_height, + ) + .map_err(|e| { + error!( + target: LOG_TARGET, + "Transaction (TxId: {}) could not be finalized. Failure error: {:?}", tx_id, e, + ); + TransactionServiceProtocolError::new(tx_id, e.into()) + })?; info!(target: LOG_TARGET, "Finalized one-side transaction TxId: {}", tx_id); // This event being sent is important, but not critical to the protocol being successful. Send only fails if @@ -908,7 +926,9 @@ where let fee = stp .get_fee_amount() .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; - + self.output_manager_service + .add_output_with_tx_id(tx_id, unblinded_output, Some(SpendingPriority::HtlcSpendAsap)) + .await?; self.submit_transaction( transaction_broadcast_join_handles, CompletedTransaction::new( @@ -987,13 +1007,13 @@ where .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; // TODO: Add a standardized Diffie-Hellman method to the tari_crypto library that will return a private key, // TODO: then come back and use it here. - let spending_key = PrivateKey::from_bytes( + let spend_key = PrivateKey::from_bytes( CommsPublicKey::shared_secret(&sender_offset_private_key.clone(), &dest_pubkey.clone()).as_bytes(), ) .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; let sender_message = TransactionSenderMessage::new_single_round_message(stp.get_single_round_message()?); - let rewind_key = PrivateKey::from_bytes(&hash_secret_key(&spending_key))?; + let rewind_key = PrivateKey::from_bytes(&hash_secret_key(&spend_key))?; let blinding_key = PrivateKey::from_bytes(&hash_secret_key(&rewind_key))?; let rewind_data = RewindData { rewind_key: rewind_key.clone(), @@ -1004,7 +1024,7 @@ where let rtp = ReceiverTransactionProtocol::new_with_rewindable_output( sender_message, PrivateKey::random(&mut OsRng), - spending_key, + spend_key, OutputFeatures::default(), &self.resources.factories, &rewind_data, @@ -1019,14 +1039,19 @@ where // Finalize - stp.finalize(KernelFeatures::empty(), &self.resources.factories) - .map_err(|e| { - error!( - target: LOG_TARGET, - "Transaction (TxId: {}) could not be finalized. Failure error: {:?}", tx_id, e, - ); - TransactionServiceProtocolError::new(tx_id, e.into()) - })?; + stp.finalize( + KernelFeatures::empty(), + &self.resources.factories, + None, + self.last_seen_tip_height, + ) + .map_err(|e| { + error!( + target: LOG_TARGET, + "Transaction (TxId: {}) could not be finalized. Failure error: {:?}", tx_id, e, + ); + TransactionServiceProtocolError::new(tx_id, e.into()) + })?; info!(target: LOG_TARGET, "Finalized one-side transaction TxId: {}", tx_id); // This event being sent is important, but not critical to the protocol being successful. Send only fails if @@ -1290,19 +1315,6 @@ where Ok(()) } - // async fn set_completed_transaction_validity( - // &mut self, - // tx_id: TxId, - // valid: bool, - // ) -> Result<(), TransactionServiceError> { - // self.resources - // .db - // .set_completed_transaction_validity(tx_id, valid) - // .await?; - // - // Ok(()) - // } - /// Handle a Transaction Cancelled message received from the Comms layer pub async fn handle_transaction_cancelled_message( &mut self, @@ -1356,6 +1368,8 @@ where tx.message, None, TransactionSendProtocolStage::WaitForReply, + None, + self.last_seen_tip_height, ); let join_handle = tokio::spawn(protocol.execute()); @@ -1478,6 +1492,8 @@ where self.resources.clone(), tx_finalized_receiver, cancellation_receiver, + None, + self.last_seen_tip_height, ); let join_handle = tokio::spawn(protocol.execute()); @@ -1662,6 +1678,8 @@ where self.resources.clone(), tx_finalized_receiver, cancellation_receiver, + None, + self.last_seen_tip_height, ); let join_handle = tokio::spawn(protocol.execute()); diff --git a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs index 52dcf91cd4..45c6370855 100644 --- a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs @@ -2145,7 +2145,7 @@ mod test { ) .with_change_script(script!(Nop), ExecutionStack::default(), PrivateKey::random(&mut OsRng)); - let mut stp = builder.build::(&factories).unwrap(); + let mut stp = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); let outbound_tx1 = OutboundTransaction { tx_id: 1u64, diff --git a/base_layer/wallet/src/wallet.rs b/base_layer/wallet/src/wallet.rs index 1ea209b069..11b044b65b 100644 --- a/base_layer/wallet/src/wallet.rs +++ b/base_layer/wallet/src/wallet.rs @@ -355,6 +355,7 @@ where metadata_signature: ComSignature, script_private_key: &PrivateKey, sender_offset_public_key: &PublicKey, + script_lock_height: u64, ) -> Result { let unblinded_output = UnblindedOutput::new( amount, @@ -365,6 +366,7 @@ where script_private_key.clone(), sender_offset_public_key.clone(), metadata_signature, + script_lock_height, ); let tx_id = self @@ -373,7 +375,7 @@ where .await?; self.output_manager_service - .add_unvalidated_output(tx_id, unblinded_output.clone()) + .add_unvalidated_output(tx_id, unblinded_output.clone(), None) .await?; info!( @@ -408,7 +410,7 @@ where .await?; self.output_manager_service - .add_output_with_tx_id(tx_id, unblinded_output.clone()) + .add_output_with_tx_id(tx_id, unblinded_output.clone(), None) .await?; info!( @@ -559,6 +561,7 @@ pub async fn persist_one_sided_payment_script_for_node_identity( private_key: node_identity.secret_key().clone(), script, input: ExecutionStack::default(), + script_lock_height: 0, }; output_manager_service.add_known_script(known_script).await?; diff --git a/base_layer/wallet/tests/output_manager_service/service.rs b/base_layer/wallet/tests/output_manager_service/service.rs index 892c99c5c4..a420a1c4ca 100644 --- a/base_layer/wallet/tests/output_manager_service/service.rs +++ b/base_layer/wallet/tests/output_manager_service/service.rs @@ -311,7 +311,7 @@ fn generate_sender_transaction_message(amount: MicroTari) -> (TxId, TransactionS script_private_key, ); - let mut stp = builder.build::(&factories).unwrap(); + let mut stp = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); let tx_id = stp.get_tx_id().unwrap(); ( tx_id, @@ -328,7 +328,7 @@ async fn fee_estimate() { let (mut oms, _, _shutdown, _, _, _, _, _) = setup_output_manager_service(backend, true).await; let (_, uo) = make_input(&mut OsRng.clone(), MicroTari::from(3000), &factories.commitment); - oms.add_output(uo).await.unwrap(); + oms.add_output(uo, None).await.unwrap(); let fee_calc = Fee::new(*create_consensus_constants(0).transaction_weight()); // minimum fpg let fee_per_gram = MicroTari::from(1); @@ -407,7 +407,7 @@ async fn test_utxo_selection_no_chain_metadata() { &factories.commitment, Some(OutputFeatures::with_maturity(i)), ); - oms.add_output(uo.clone()).await.unwrap(); + oms.add_output(uo.clone(), None).await.unwrap(); } // but we have no chain state so the lowest maturity should be used @@ -509,7 +509,7 @@ async fn test_utxo_selection_with_chain_metadata() { &factories.commitment, Some(OutputFeatures::with_maturity(i)), ); - oms.add_output(uo.clone()).await.unwrap(); + oms.add_output(uo.clone(), None).await.unwrap(); } let utxos = oms.get_unspent_outputs().await.unwrap(); @@ -605,7 +605,7 @@ async fn send_not_enough_funds() { MicroTari::from(200 + OsRng.next_u64() % 1000), &factories.commitment, ); - oms.add_output(uo).await.unwrap(); + oms.add_output(uo, None).await.unwrap(); } match oms @@ -635,21 +635,27 @@ async fn send_no_change() { let constants = create_consensus_constants(0); let fee_without_change = Fee::new(*constants.transaction_weight()).calculate(fee_per_gram, 1, 2, 1, 0); let value1 = 500; - oms.add_output(create_unblinded_output( - script!(Nop), - OutputFeatures::default(), - TestParamsHelpers::new(), - MicroTari::from(value1), - )) + oms.add_output( + create_unblinded_output( + script!(Nop), + OutputFeatures::default(), + TestParamsHelpers::new(), + MicroTari::from(value1), + ), + None, + ) .await .unwrap(); let value2 = 800; - oms.add_output(create_unblinded_output( - script!(Nop), - OutputFeatures::default(), - TestParamsHelpers::new(), - MicroTari::from(value2), - )) + oms.add_output( + create_unblinded_output( + script!(Nop), + OutputFeatures::default(), + TestParamsHelpers::new(), + MicroTari::from(value2), + ), + None, + ) .await .unwrap(); @@ -682,21 +688,27 @@ async fn send_not_enough_for_change() { let constants = create_consensus_constants(0); let fee_without_change = Fee::new(*constants.transaction_weight()).calculate(fee_per_gram, 1, 2, 1, 0); let value1 = MicroTari(500); - oms.add_output(create_unblinded_output( - TariScript::default(), - OutputFeatures::default(), - TestParamsHelpers::new(), - value1, - )) + oms.add_output( + create_unblinded_output( + TariScript::default(), + OutputFeatures::default(), + TestParamsHelpers::new(), + value1, + ), + None, + ) .await .unwrap(); let value2 = MicroTari(800); - oms.add_output(create_unblinded_output( - TariScript::default(), - OutputFeatures::default(), - TestParamsHelpers::new(), - value2, - )) + oms.add_output( + create_unblinded_output( + TariScript::default(), + OutputFeatures::default(), + TestParamsHelpers::new(), + value2, + ), + None, + ) .await .unwrap(); @@ -732,7 +744,7 @@ async fn cancel_transaction() { MicroTari::from(100 + OsRng.next_u64() % 1000), &factories.commitment, ); - oms.add_output(uo).await.unwrap(); + oms.add_output(uo, None).await.unwrap(); } let stp = oms .prepare_transaction_to_send( @@ -802,11 +814,11 @@ async fn test_get_balance() { let output_val = MicroTari::from(2000); let (_ti, uo) = make_input(&mut OsRng.clone(), output_val, &factories.commitment); total += uo.value; - oms.add_output(uo).await.unwrap(); + oms.add_output(uo, None).await.unwrap(); let (_ti, uo) = make_input(&mut OsRng.clone(), output_val, &factories.commitment); total += uo.value; - oms.add_output(uo).await.unwrap(); + oms.add_output(uo, None).await.unwrap(); let send_value = MicroTari::from(1000); let stp = oms @@ -830,7 +842,7 @@ async fn test_get_balance() { let balance = oms.get_balance().await.unwrap(); assert_eq!(output_val, balance.available_balance); - assert_eq!(output_val, balance.time_locked_balance.unwrap()); + assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); assert_eq!(recv_value + change_val, balance.pending_incoming_balance); assert_eq!(output_val, balance.pending_outgoing_balance); } @@ -846,11 +858,11 @@ async fn sending_transaction_with_short_term_clear() { let available_balance = 10_000 * uT; let (_ti, uo) = make_input(&mut OsRng.clone(), available_balance, &factories.commitment); - oms.add_output(uo).await.unwrap(); + oms.add_output(uo, None).await.unwrap(); let balance = oms.get_balance().await.unwrap(); assert_eq!(balance.available_balance, available_balance); - assert_eq!(balance.time_locked_balance.unwrap(), available_balance); + assert_eq!(balance.time_locked_balance.unwrap(), MicroTari::from(0)); // Check that funds are encumbered and then unencumbered if the pending tx is not confirmed before restart let _stp = oms @@ -875,7 +887,7 @@ async fn sending_transaction_with_short_term_clear() { let balance = oms.get_balance().await.unwrap(); assert_eq!(balance.available_balance, available_balance); - assert_eq!(balance.time_locked_balance.unwrap(), available_balance); + assert_eq!(balance.time_locked_balance.unwrap(), MicroTari::from(0)); // Check that is the pending tx is confirmed that the encumberance persists after restart let stp = oms @@ -914,9 +926,9 @@ async fn coin_split_with_change() { let (_ti, uo1) = make_input(&mut OsRng, val1, &factories.commitment); let (_ti, uo2) = make_input(&mut OsRng, val2, &factories.commitment); let (_ti, uo3) = make_input(&mut OsRng, val3, &factories.commitment); - assert!(oms.add_output(uo1).await.is_ok()); - assert!(oms.add_output(uo2).await.is_ok()); - assert!(oms.add_output(uo3).await.is_ok()); + assert!(oms.add_output(uo1, None).await.is_ok()); + assert!(oms.add_output(uo2, None).await.is_ok()); + assert!(oms.add_output(uo3, None).await.is_ok()); let fee_per_gram = MicroTari::from(5); let split_count = 8; @@ -962,9 +974,9 @@ async fn coin_split_no_change() { let (_ti, uo1) = make_input(&mut OsRng, val1, &factories.commitment); let (_ti, uo2) = make_input(&mut OsRng, val2, &factories.commitment); let (_ti, uo3) = make_input(&mut OsRng, val3, &factories.commitment); - assert!(oms.add_output(uo1).await.is_ok()); - assert!(oms.add_output(uo2).await.is_ok()); - assert!(oms.add_output(uo3).await.is_ok()); + assert!(oms.add_output(uo1, None).await.is_ok()); + assert!(oms.add_output(uo2, None).await.is_ok()); + assert!(oms.add_output(uo3, None).await.is_ok()); let (_tx_id, coin_split_tx, fee, amount) = oms .create_coin_split(1000.into(), split_count, fee_per_gram, None) @@ -1053,7 +1065,7 @@ async fn test_txo_validation() { MicroTari::from(output1_value), ); let output1_tx_output = output1.as_transaction_output(&factories).unwrap(); - oms.add_output_with_tx_id(1, output1.clone()).await.unwrap(); + oms.add_output_with_tx_id(1, output1.clone(), None).await.unwrap(); let output2_value = 2_000_000; let output2 = create_unblinded_output( @@ -1064,7 +1076,7 @@ async fn test_txo_validation() { ); let output2_tx_output = output2.as_transaction_output(&factories).unwrap(); - oms.add_output_with_tx_id(2, output2.clone()).await.unwrap(); + oms.add_output_with_tx_id(2, output2.clone(), None).await.unwrap(); let output3_value = 4_000_000; let output3 = create_unblinded_output( @@ -1074,7 +1086,7 @@ async fn test_txo_validation() { MicroTari::from(output3_value), ); - oms.add_output_with_tx_id(3, output3.clone()).await.unwrap(); + oms.add_output_with_tx_id(3, output3.clone(), None).await.unwrap(); let mut block1_header = BlockHeader::new(1); block1_header.height = 1; @@ -1173,11 +1185,12 @@ async fn test_txo_validation() { let output6_tx_output = output6.unblinded_output.as_transaction_output(&factories).unwrap(); let balance = oms.get_balance().await.unwrap(); + assert_eq!( balance.available_balance, MicroTari::from(output2_value) + MicroTari::from(output3_value) ); - assert_eq!(balance.available_balance, balance.time_locked_balance.unwrap()); + assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); assert_eq!(balance.pending_outgoing_balance, MicroTari::from(output1_value)); assert_eq!( balance.pending_incoming_balance, @@ -1278,7 +1291,7 @@ async fn test_txo_validation() { balance.available_balance, MicroTari::from(output2_value) + MicroTari::from(output3_value) ); - assert_eq!(balance.available_balance, balance.time_locked_balance.unwrap()); + assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); assert_eq!(oms.get_unspent_outputs().await.unwrap().len(), 2); @@ -1327,7 +1340,7 @@ async fn test_txo_validation() { ); assert_eq!(balance.pending_outgoing_balance, MicroTari::from(0)); assert_eq!(balance.pending_incoming_balance, MicroTari::from(0)); - assert_eq!(balance.available_balance, balance.time_locked_balance.unwrap()); + assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); // Trigger another validation and only Output3 should be checked oms.validate_txos().await.unwrap(); @@ -1450,7 +1463,7 @@ async fn test_txo_validation() { balance.pending_incoming_balance, MicroTari::from(output1_value) - MicroTari::from(901_240) ); - assert_eq!(balance.available_balance, balance.time_locked_balance.unwrap()); + assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); // Now we will update the mined_height in the responses so that the outputs on the reorged chain are confirmed // Output 1: Spent in Block 5 - Confirmed @@ -1510,7 +1523,7 @@ async fn test_txo_validation() { ); assert_eq!(balance.pending_outgoing_balance, MicroTari::from(0)); assert_eq!(balance.pending_incoming_balance, MicroTari::from(0)); - assert_eq!(balance.available_balance, balance.time_locked_balance.unwrap()); + assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); } #[tokio::test] @@ -1546,7 +1559,7 @@ async fn test_txo_revalidation() { MicroTari::from(output1_value), ); let output1_tx_output = output1.as_transaction_output(&factories).unwrap(); - oms.add_output_with_tx_id(1, output1.clone()).await.unwrap(); + oms.add_output_with_tx_id(1, output1.clone(), None).await.unwrap(); let output2_value = 2_000_000; let output2 = create_unblinded_output( @@ -1557,7 +1570,7 @@ async fn test_txo_revalidation() { ); let output2_tx_output = output2.as_transaction_output(&factories).unwrap(); - oms.add_output_with_tx_id(2, output2.clone()).await.unwrap(); + oms.add_output_with_tx_id(2, output2.clone(), None).await.unwrap(); let mut block1_header = BlockHeader::new(1); block1_header.height = 1; diff --git a/base_layer/wallet/tests/output_manager_service/storage.rs b/base_layer/wallet/tests/output_manager_service/storage.rs index 09ba45d115..d721fbc988 100644 --- a/base_layer/wallet/tests/output_manager_service/storage.rs +++ b/base_layer/wallet/tests/output_manager_service/storage.rs @@ -55,7 +55,7 @@ pub fn test_db_backend(backend: T) { MicroTari::from(100 + OsRng.next_u64() % 1000), &factories.commitment, ); - let mut uo = DbUnblindedOutput::from_unblinded_output(uo, &factories).unwrap(); + let mut uo = DbUnblindedOutput::from_unblinded_output(uo, &factories, None).unwrap(); uo.unblinded_output.features.maturity = i; runtime.block_on(db.add_unspent_output(uo.clone())).unwrap(); unspent_outputs.push(uo); @@ -102,7 +102,7 @@ pub fn test_db_backend(backend: T) { MicroTari::from(100 + OsRng.next_u64() % 1000), &factories.commitment, ); - let uo = DbUnblindedOutput::from_unblinded_output(uo, &factories).unwrap(); + let uo = DbUnblindedOutput::from_unblinded_output(uo, &factories, None).unwrap(); runtime.block_on(db.add_unspent_output(uo.clone())).unwrap(); pending_tx.outputs_to_be_spent.push(uo); } @@ -112,7 +112,7 @@ pub fn test_db_backend(backend: T) { MicroTari::from(100 + OsRng.next_u64() % 1000), &factories.commitment, ); - let uo = DbUnblindedOutput::from_unblinded_output(uo, &factories).unwrap(); + let uo = DbUnblindedOutput::from_unblinded_output(uo, &factories, None).unwrap(); pending_tx.outputs_to_be_received.push(uo); } runtime @@ -254,7 +254,7 @@ pub fn test_db_backend(backend: T) { MicroTari::from(100 + OsRng.next_u64() % 1000), &factories.commitment, ); - let output_to_be_received = DbUnblindedOutput::from_unblinded_output(uo, &factories).unwrap(); + let output_to_be_received = DbUnblindedOutput::from_unblinded_output(uo, &factories, None).unwrap(); runtime .block_on(db.add_output_to_be_received(11, output_to_be_received.clone(), None)) .unwrap(); @@ -381,7 +381,7 @@ pub async fn test_short_term_encumberance() { MicroTari::from(100 + OsRng.next_u64() % 1000), &factories.commitment, ); - let mut uo = DbUnblindedOutput::from_unblinded_output(uo, &factories).unwrap(); + let mut uo = DbUnblindedOutput::from_unblinded_output(uo, &factories, None).unwrap(); uo.unblinded_output.features.maturity = i; db.add_unspent_output(uo.clone()).await.unwrap(); unspent_outputs.push(uo); @@ -434,7 +434,7 @@ pub async fn test_no_duplicate_outputs() { // create an output let (_ti, uo) = make_input(&mut OsRng, MicroTari::from(1000), &factories.commitment); - let uo = DbUnblindedOutput::from_unblinded_output(uo, &factories).unwrap(); + let uo = DbUnblindedOutput::from_unblinded_output(uo, &factories, None).unwrap(); // add it to the database let result = db.add_unspent_output(uo.clone()).await; diff --git a/base_layer/wallet/tests/support/comms_rpc.rs b/base_layer/wallet/tests/support/comms_rpc.rs index 3a4f306fc4..05188342f8 100644 --- a/base_layer/wallet/tests/support/comms_rpc.rs +++ b/base_layer/wallet/tests/support/comms_rpc.rs @@ -131,7 +131,7 @@ impl BaseNodeWalletRpcMockState { })), tip_info_response: Arc::new(Mutex::new(TipInfoResponse { metadata: Some(ChainMetadataProto { - height_of_longest_chain: Some(std::u64::MAX), + height_of_longest_chain: Some(std::i64::MAX as u64), best_block: Some(Vec::new()), accumulated_difficulty: Vec::new(), pruned_height: 0, diff --git a/base_layer/wallet/tests/transaction_service/service.rs b/base_layer/wallet/tests/transaction_service/service.rs index acebd6425a..3d25188ddc 100644 --- a/base_layer/wallet/tests/transaction_service/service.rs +++ b/base_layer/wallet/tests/transaction_service/service.rs @@ -20,14 +20,6 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::{ - collections::HashMap, - convert::{TryFrom, TryInto}, - path::Path, - sync::Arc, - time::Duration, -}; - use chrono::{Duration as ChronoDuration, Utc}; use futures::{ channel::{mpsc, mpsc::Sender}, @@ -36,6 +28,13 @@ use futures::{ }; use prost::Message; use rand::{rngs::OsRng, RngCore}; +use std::{ + collections::HashMap, + convert::{TryFrom, TryInto}, + path::Path, + sync::Arc, + time::Duration, +}; use tari_crypto::{ commitment::HomomorphicCommitmentFactory, common::Blake256, @@ -193,7 +192,7 @@ pub fn setup_transaction_service>( )); let db = WalletDatabase::new(WalletSqliteDatabase::new(db_connection.clone(), None).unwrap()); - let metadata = ChainMetadata::new(std::u64::MAX, Vec::new(), 0, 0, 0); + let metadata = ChainMetadata::new(std::i64::MAX as u64, Vec::new(), 0, 0, 0); runtime.block_on(db.set_chain_metadata(metadata)).unwrap(); @@ -540,7 +539,7 @@ fn manage_single_transaction() { )) .is_err()); - runtime.block_on(alice_oms.add_output(uo1)).unwrap(); + runtime.block_on(alice_oms.add_output(uo1, None)).unwrap(); let message = "TAKE MAH MONEYS!".to_string(); runtime .block_on(alice_ts.send_transaction( @@ -655,7 +654,7 @@ fn single_transaction_to_self() { let initial_wallet_value = 2500.into(); let (_utxo, uo1) = make_input(&mut OsRng, initial_wallet_value, &factories.commitment); - alice_oms.add_output(uo1).await.unwrap(); + alice_oms.add_output(uo1, None).await.unwrap(); let message = "TAKE MAH _OWN_ MONEYS!".to_string(); let value = 1000.into(); let tx_id = alice_ts @@ -738,7 +737,7 @@ fn send_one_sided_transaction_to_other() { let initial_wallet_value = 2500.into(); let (_utxo, uo1) = make_input(&mut OsRng, initial_wallet_value, &factories.commitment); let mut alice_oms_clone = alice_oms.clone(); - runtime.block_on(async move { alice_oms_clone.add_output(uo1).await.unwrap() }); + runtime.block_on(async move { alice_oms_clone.add_output(uo1, None).await.unwrap() }); let message = "SEE IF YOU CAN CATCH THIS ONE..... SIDED TX!".to_string(); let value = 1000.into(); @@ -860,6 +859,7 @@ fn recover_one_sided_transaction() { private_key: bob_node_identity.secret_key().clone(), script, input: ExecutionStack::default(), + script_lock_height: 0, }; let mut cloned_bob_oms = bob_oms.clone(); runtime.block_on(async move { @@ -871,7 +871,7 @@ fn recover_one_sided_transaction() { let initial_wallet_value = 2500.into(); let (_utxo, uo1) = make_input(&mut OsRng, initial_wallet_value, &factories.commitment); let mut alice_oms_clone = alice_oms; - runtime.block_on(async move { alice_oms_clone.add_output(uo1).await.unwrap() }); + runtime.block_on(async move { alice_oms_clone.add_output(uo1, None).await.unwrap() }); let message = "".to_string(); let value = 1000.into(); @@ -944,7 +944,7 @@ fn test_htlc_send_and_claim() { let bob_connection = run_migration_and_create_sqlite_connection(&bob_db_path, 16).unwrap(); let shutdown = Shutdown::new(); - let (alice_ts, alice_oms, _alice_comms, mut alice_connectivity) = setup_transaction_service( + let (mut alice_ts, mut alice_oms, _alice_comms, mut alice_connectivity) = setup_transaction_service( &mut runtime, alice_node_identity, vec![], @@ -985,7 +985,7 @@ fn test_htlc_send_and_claim() { let initial_wallet_value = 2500.into(); let (_utxo, uo1) = make_input(&mut OsRng, initial_wallet_value, &factories.commitment); let mut alice_oms_clone = alice_oms.clone(); - runtime.block_on(async move { alice_oms_clone.add_output(uo1).await.unwrap() }); + runtime.block_on(async move { alice_oms_clone.add_output(uo1, None).await.unwrap() }); let message = "".to_string(); let value = 1000.into(); @@ -998,10 +998,8 @@ fn test_htlc_send_and_claim() { .expect("Alice sending HTLC transaction") }); - let mut alice_ts_clone2 = alice_ts; - let mut alice_oms_clone = alice_oms; runtime.block_on(async move { - let completed_tx = alice_ts_clone2 + let completed_tx = alice_ts .get_completed_transaction(tx_id) .await .expect("Could not find completed HTLC tx"); @@ -1009,7 +1007,7 @@ fn test_htlc_send_and_claim() { let fees = completed_tx.fee; assert_eq!( - alice_oms_clone.get_balance().await.unwrap().pending_incoming_balance, + alice_oms.get_balance().await.unwrap().pending_incoming_balance, initial_wallet_value - value - fees ); }); @@ -1097,7 +1095,7 @@ fn send_one_sided_transaction_to_self() { let initial_wallet_value = 2500.into(); let (_utxo, uo1) = make_input(&mut OsRng, initial_wallet_value, &factories.commitment); let mut alice_oms_clone = alice_oms; - runtime.block_on(async move { alice_oms_clone.add_output(uo1).await.unwrap() }); + runtime.block_on(async move { alice_oms_clone.add_output(uo1, None).await.unwrap() }); let message = "SEE IF YOU CAN CATCH THIS ONE..... SIDED TX!".to_string(); let value = 1000.into(); @@ -1223,17 +1221,17 @@ fn manage_multiple_transactions() { ); let (_utxo, uo2) = make_input(&mut OsRng, MicroTari(3500), &factories.commitment); - runtime.block_on(bob_oms.add_output(uo2)).unwrap(); + runtime.block_on(bob_oms.add_output(uo2, None)).unwrap(); let (_utxo, uo3) = make_input(&mut OsRng, MicroTari(4500), &factories.commitment); - runtime.block_on(carol_oms.add_output(uo3)).unwrap(); + runtime.block_on(carol_oms.add_output(uo3, None)).unwrap(); // Add some funds to Alices wallet let (_utxo, uo1a) = make_input(&mut OsRng, MicroTari(5500), &factories.commitment); - runtime.block_on(alice_oms.add_output(uo1a)).unwrap(); + runtime.block_on(alice_oms.add_output(uo1a, None)).unwrap(); let (_utxo, uo1b) = make_input(&mut OsRng, MicroTari(3000), &factories.commitment); - runtime.block_on(alice_oms.add_output(uo1b)).unwrap(); + runtime.block_on(alice_oms.add_output(uo1b, None)).unwrap(); let (_utxo, uo1c) = make_input(&mut OsRng, MicroTari(3000), &factories.commitment); - runtime.block_on(alice_oms.add_output(uo1c)).unwrap(); + runtime.block_on(alice_oms.add_output(uo1c, None)).unwrap(); // A series of interleaved transactions. First with Bob and Carol offline and then two with them online let value_a_to_b_1 = MicroTari::from(1000); @@ -1412,7 +1410,7 @@ fn test_accepting_unknown_tx_id_and_malformed_reply() { let (_utxo, uo) = make_input(&mut OsRng, MicroTari(250000), &factories.commitment); - runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + runtime.block_on(alice_output_manager.add_output(uo, None)).unwrap(); runtime .block_on(alice_ts.send_transaction( @@ -1537,7 +1535,7 @@ fn finalize_tx_with_incorrect_pubkey() { ) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), connection_bob, None); let (_utxo, uo) = make_input(&mut OsRng, MicroTari(250000), &factories.commitment); - runtime.block_on(bob_output_manager.add_output(uo)).unwrap(); + runtime.block_on(bob_output_manager.add_output(uo, None)).unwrap(); let mut stp = runtime .block_on(bob_output_manager.prepare_transaction_to_send( OsRng.next_u64(), @@ -1571,7 +1569,8 @@ fn finalize_tx_with_incorrect_pubkey() { stp.add_single_recipient_info(recipient_reply.clone(), &factories.range_proof) .unwrap(); - stp.finalize(KernelFeatures::empty(), &factories).unwrap(); + stp.finalize(KernelFeatures::empty(), &factories, None, Some(u64::MAX)) + .unwrap(); let tx = stp.get_transaction().unwrap(); let finalized_transaction_message = proto::TransactionFinalizedMessage { @@ -1666,7 +1665,7 @@ fn finalize_tx_with_missing_output() { let (_utxo, uo) = make_input(&mut OsRng, MicroTari(250000), &factories.commitment); - runtime.block_on(bob_output_manager.add_output(uo)).unwrap(); + runtime.block_on(bob_output_manager.add_output(uo, None)).unwrap(); let mut stp = runtime .block_on(bob_output_manager.prepare_transaction_to_send( @@ -1701,7 +1700,8 @@ fn finalize_tx_with_missing_output() { stp.add_single_recipient_info(recipient_reply.clone(), &factories.range_proof) .unwrap(); - stp.finalize(KernelFeatures::empty(), &factories).unwrap(); + stp.finalize(KernelFeatures::empty(), &factories, None, Some(u64::MAX)) + .unwrap(); let finalized_transaction_message = proto::TransactionFinalizedMessage { tx_id: recipient_reply.tx_id, @@ -1817,11 +1817,11 @@ fn discovery_async_return_test() { let mut alice_event_stream = alice_ts.get_event_stream(); let (_utxo, uo1a) = make_input(&mut OsRng, MicroTari(5500), &factories.commitment); - runtime.block_on(alice_oms.add_output(uo1a)).unwrap(); + runtime.block_on(alice_oms.add_output(uo1a, None)).unwrap(); let (_utxo, uo1b) = make_input(&mut OsRng, MicroTari(3000), &factories.commitment); - runtime.block_on(alice_oms.add_output(uo1b)).unwrap(); + runtime.block_on(alice_oms.add_output(uo1b, None)).unwrap(); let (_utxo, uo1c) = make_input(&mut OsRng, MicroTari(3000), &factories.commitment); - runtime.block_on(alice_oms.add_output(uo1c)).unwrap(); + runtime.block_on(alice_oms.add_output(uo1c, None)).unwrap(); let initial_balance = runtime.block_on(alice_oms.get_balance()).unwrap(); @@ -2126,7 +2126,7 @@ fn test_transaction_cancellation() { let alice_total_available = 250000 * uT; let (_utxo, uo) = make_input(&mut OsRng, alice_total_available, &factories.commitment); - runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + runtime.block_on(alice_output_manager.add_output(uo, None)).unwrap(); let amount_sent = 10000 * uT; @@ -2248,7 +2248,7 @@ fn test_transaction_cancellation() { ) .with_change_script(script!(Nop), ExecutionStack::default(), PrivateKey::random(&mut OsRng)); - let mut stp = builder.build::(&factories).unwrap(); + let mut stp = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); let tx_sender_msg = stp.build_single_round_message().unwrap(); let tx_id2 = tx_sender_msg.tx_id; let proto_message = proto::TransactionSenderMessage::single(tx_sender_msg.into()); @@ -2320,7 +2320,7 @@ fn test_transaction_cancellation() { ) .with_change_script(script!(Nop), ExecutionStack::default(), PrivateKey::random(&mut OsRng)); - let mut stp = builder.build::(&factories).unwrap(); + let mut stp = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); let tx_sender_msg = stp.build_single_round_message().unwrap(); let tx_id3 = tx_sender_msg.tx_id; let proto_message = proto::TransactionSenderMessage::single(tx_sender_msg.into()); @@ -2431,7 +2431,7 @@ fn test_direct_vs_saf_send_of_tx_reply_and_finalize() { let alice_total_available = 250000 * uT; let (_utxo, uo) = make_input(&mut OsRng, alice_total_available, &factories.commitment); - runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + runtime.block_on(alice_output_manager.add_output(uo, None)).unwrap(); let amount_sent = 10000 * uT; @@ -2603,7 +2603,7 @@ fn test_direct_vs_saf_send_of_tx_reply_and_finalize() { // Now to repeat sending so we can test the SAF send of the finalize message let alice_total_available = 250000 * uT; let (_utxo, uo) = make_input(&mut OsRng, alice_total_available, &factories.commitment); - runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + runtime.block_on(alice_output_manager.add_output(uo, None)).unwrap(); let amount_sent = 20000 * uT; @@ -2701,13 +2701,13 @@ fn test_tx_direct_send_behaviour() { let mut alice_event_stream = alice_ts.get_event_stream(); let (_utxo, uo) = make_input(&mut OsRng, 1000000 * uT, &factories.commitment); - runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + runtime.block_on(alice_output_manager.add_output(uo, None)).unwrap(); let (_utxo, uo) = make_input(&mut OsRng, 1000000 * uT, &factories.commitment); - runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + runtime.block_on(alice_output_manager.add_output(uo, None)).unwrap(); let (_utxo, uo) = make_input(&mut OsRng, 1000000 * uT, &factories.commitment); - runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + runtime.block_on(alice_output_manager.add_output(uo, None)).unwrap(); let (_utxo, uo) = make_input(&mut OsRng, 1000000 * uT, &factories.commitment); - runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + runtime.block_on(alice_output_manager.add_output(uo, None)).unwrap(); let amount_sent = 10000 * uT; @@ -2939,7 +2939,7 @@ fn test_restarting_transaction_protocols() { inputs!(PublicKey::from_secret_key(&script_private_key)), script_private_key, ); - let mut bob_stp = builder.build::(&factories).unwrap(); + let mut bob_stp = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); let msg = bob_stp.build_single_round_message().unwrap(); let bob_pre_finalize = bob_stp.clone(); @@ -2960,7 +2960,7 @@ fn test_restarting_transaction_protocols() { .add_single_recipient_info(alice_reply.clone(), &factories.range_proof) .unwrap(); - match bob_stp.finalize(KernelFeatures::empty(), &factories) { + match bob_stp.finalize(KernelFeatures::empty(), &factories, None, Some(u64::MAX)) { Ok(_) => (), Err(e) => panic!("Should be able to finalize tx: {}", e), }; @@ -3914,7 +3914,7 @@ fn test_transaction_resending() { // Send a transaction to Bob let alice_total_available = 250000 * uT; let (_utxo, uo) = make_input(&mut OsRng, alice_total_available, &factories.commitment); - runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + runtime.block_on(alice_output_manager.add_output(uo, None)).unwrap(); let amount_sent = 10000 * uT; @@ -4112,7 +4112,7 @@ fn test_resend_on_startup() { ) .with_change_script(script!(Nop), ExecutionStack::default(), PrivateKey::random(&mut OsRng)); - let mut stp = builder.build::(&factories).unwrap(); + let mut stp = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); let stp_msg = stp.build_single_round_message().unwrap(); let tx_sender_msg = TransactionSenderMessage::Single(Box::new(stp_msg)); @@ -4410,7 +4410,7 @@ fn test_replying_to_cancelled_tx() { // Send a transaction to Bob let alice_total_available = 250000 * uT; let (_utxo, uo) = make_input(&mut OsRng, alice_total_available, &factories.commitment); - runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + runtime.block_on(alice_output_manager.add_output(uo, None)).unwrap(); let amount_sent = 10000 * uT; @@ -4544,7 +4544,7 @@ fn test_transaction_timeout_cancellation() { // Send a transaction to Bob let alice_total_available = 250000 * uT; let (_utxo, uo) = make_input(&mut OsRng, alice_total_available, &factories.commitment); - runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + runtime.block_on(alice_output_manager.add_output(uo, None)).unwrap(); let amount_sent = 10000 * uT; @@ -4619,7 +4619,7 @@ fn test_transaction_timeout_cancellation() { ) .with_change_script(script!(Nop), ExecutionStack::default(), PrivateKey::random(&mut OsRng)); - let mut stp = builder.build::(&factories).unwrap(); + let mut stp = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); let stp_msg = stp.build_single_round_message().unwrap(); let tx_sender_msg = TransactionSenderMessage::Single(Box::new(stp_msg)); @@ -4828,10 +4828,10 @@ fn transaction_service_tx_broadcast() { let alice_output_value = MicroTari(250000); let (_utxo, uo) = make_input(&mut OsRng, alice_output_value, &factories.commitment); - runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + runtime.block_on(alice_output_manager.add_output(uo, None)).unwrap(); let (_utxo, uo2) = make_input(&mut OsRng, alice_output_value, &factories.commitment); - runtime.block_on(alice_output_manager.add_output(uo2)).unwrap(); + runtime.block_on(alice_output_manager.add_output(uo2, None)).unwrap(); let amount_sent1 = 10000 * uT; diff --git a/base_layer/wallet/tests/transaction_service/storage.rs b/base_layer/wallet/tests/transaction_service/storage.rs index e14e28b8d0..54d51c3c92 100644 --- a/base_layer/wallet/tests/transaction_service/storage.rs +++ b/base_layer/wallet/tests/transaction_service/storage.rs @@ -93,7 +93,7 @@ pub fn test_db_backend(backend: T) { ) .with_change_script(script!(Nop), ExecutionStack::default(), PrivateKey::random(&mut OsRng)); - let stp = builder.build::(&factories).unwrap(); + let stp = builder.build::(&factories, None, Some(u64::MAX)).unwrap(); let messages = vec!["Hey!".to_string(), "Yo!".to_string(), "Sup!".to_string()]; let amounts = vec![MicroTari::from(10_000), MicroTari::from(23_000), MicroTari::from(5_000)]; diff --git a/base_layer/wallet/tests/wallet/mod.rs b/base_layer/wallet/tests/wallet/mod.rs index 3dcc79190b..376d7f1f5c 100644 --- a/base_layer/wallet/tests/wallet/mod.rs +++ b/base_layer/wallet/tests/wallet/mod.rs @@ -156,7 +156,7 @@ async fn create_wallet( None, None, ); - let metadata = ChainMetadata::new(std::u64::MAX, Vec::new(), 0, 0, 0); + let metadata = ChainMetadata::new(std::i64::MAX as u64, Vec::new(), 0, 0, 0); let _ = wallet_backend.write(WriteOperation::Insert(DbKeyValuePair::BaseNodeChainMetadata(metadata))); @@ -241,7 +241,7 @@ async fn test_wallet() { let value = MicroTari::from(1000); let (_utxo, uo1) = make_input(&mut OsRng, MicroTari(2500), &factories.commitment); - alice_wallet.output_manager_service.add_output(uo1).await.unwrap(); + alice_wallet.output_manager_service.add_output(uo1, None).await.unwrap(); alice_wallet .transaction_service @@ -577,7 +577,7 @@ fn test_store_and_forward_send_tx() { let (_utxo, uo1) = make_input(&mut OsRng, MicroTari(2500), &factories.commitment); alice_runtime - .block_on(alice_wallet.output_manager_service.add_output(uo1)) + .block_on(alice_wallet.output_manager_service.add_output(uo1, None)) .unwrap(); let tx_id = alice_runtime @@ -738,6 +738,7 @@ async fn test_import_utxo() { utxo.metadata_signature.clone(), &p.script_private_key, &p.sender_offset_public_key, + 0, ) .await .unwrap(); diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 43006072de..6bd2cd6136 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -4381,6 +4381,7 @@ pub unsafe extern "C" fn wallet_import_utxo( }; let public_script_key = PublicKey::from_secret_key(&(*spending_key)); + // Todo the script_lock_height can be something other than 0, for example an HTLC transaction match (*wallet).runtime.block_on((*wallet).wallet.import_utxo( MicroTari::from(amount), &(*spending_key).clone(), @@ -4392,6 +4393,7 @@ pub unsafe extern "C" fn wallet_import_utxo( ComSignature::default(), &(*spending_key).clone(), &Default::default(), + 0, )) { Ok(tx_id) => { if let Err(e) = (*wallet) diff --git a/clients/wallet_grpc_client/index.js b/clients/wallet_grpc_client/index.js index 17a5e17293..6645aa4588 100644 --- a/clients/wallet_grpc_client/index.js +++ b/clients/wallet_grpc_client/index.js @@ -44,6 +44,7 @@ function Client(address) { "revalidateAllTransactions", "SendShaAtomicSwapTransaction", "claimShaAtomicSwapTransaction", + "ClaimHtlcRefundTransaction", ]; this.waitForReady = (...args) => { diff --git a/integration_tests/features/WalletTransfer.feature b/integration_tests/features/WalletTransfer.feature index 47dd6c5914..28e68b2d5c 100644 --- a/integration_tests/features/WalletTransfer.feature +++ b/integration_tests/features/WalletTransfer.feature @@ -48,4 +48,22 @@ Feature: Wallet Transfer And mining node MINER mines 6 blocks And I claim an HTLC transaction with wallet WALLET_B at fee 20 And mining node MINER mines 6 blocks - Then I wait for wallet WALLET_B to have at least 4000000000 uT \ No newline at end of file + Then I wait for wallet WALLET_B to have at least 4000000000 uT + + Scenario: As a wallet I want to claim a HTLC refund transaction + Given I have a seed node NODE + # Add a 2nd node otherwise initial sync will not succeed + And I have 1 base nodes connected to all seed nodes + And I have wallet WALLET_A connected to all seed nodes + And I have wallet WALLET_B connected to all seed nodes + And I have wallet WALLET_C connected to all seed nodes + And I have mining node MINER connected to base node NODE and wallet WALLET_A + And I have mining node MINER_2 connected to base node NODE and wallet WALLET_C + When mining node MINER mines 10 blocks + Then I wait for wallet WALLET_A to have at least 10000000000 uT + When I broadcast HTLC transaction with 5000000000 uT from wallet WALLET_A to wallet WALLET_B at fee 20 + # atomic swaps are set at lock of 720 blocks + And mining node MINER_2 mines 720 blocks + And I claim an HTLC refund transaction with wallet WALLET_A at fee 20 + And mining node MINER_2 mines 6 blocks + Then I wait for wallet WALLET_A to have at least 9000000000 uT diff --git a/integration_tests/features/support/steps.js b/integration_tests/features/support/steps.js index 51d721570c..723da3e09e 100644 --- a/integration_tests/features/support/steps.js +++ b/integration_tests/features/support/steps.js @@ -1909,7 +1909,85 @@ When( const wait_seconds = 5; console.log( " " + - lastResult.results.failure_message + + this.lastResult.results.failure_message + + ", trying again after " + + wait_seconds + + "s (" + + retries + + " of " + + retries_limit + + ")" + ); + await sleep(wait_seconds * 1000); + retries++; + } + } + + if (success) { + this.addTransaction( + sourceInfo.public_key, + this.lastResult.results.transaction_id + ); + } + expect(success).to.equal(true); + //lets now wait for this transaction to be at least broadcast before we continue. + await waitFor( + async () => + sourceClient.isTransactionAtLeastBroadcast( + this.lastResult.results.transaction_id + ), + true, + 60 * 1000, + 5 * 1000, + 5 + ); + + let transactionPending = await sourceClient.isTransactionAtLeastBroadcast( + this.lastResult.results.transaction_id + ); + + expect(transactionPending).to.equal(true); + } +); + +When( + /I claim an HTLC refund transaction with wallet (.*) at fee (.*)/, + { timeout: 25 * 5 * 1000 }, + async function (source, feePerGram) { + const sourceClient = await this.getWallet(source).connectClient(); + + const sourceInfo = await sourceClient.identify(); + console.log("Claiming HTLC refund transaction of", source); + let success = false; + let retries = 1; + let hash = this.lastResult.output_hash; + const retries_limit = 25; + while (!success && retries <= retries_limit) { + await waitFor( + async () => { + try { + this.lastResult = await sourceClient.claimHtlcRefund({ + output_hash: hash, + fee_per_gram: feePerGram, + }); + } catch (error) { + console.log(error); + return false; + } + return true; + }, + true, + 20 * 1000, + 5 * 1000, + 5 + ); + + success = this.lastResult.results.is_success; + if (!success) { + const wait_seconds = 5; + console.log( + " " + + this.lastResult.results.failure_message + ", trying again after " + wait_seconds + "s (" + diff --git a/integration_tests/helpers/walletClient.js b/integration_tests/helpers/walletClient.js index 9bbd06c639..e2ed88bd0a 100644 --- a/integration_tests/helpers/walletClient.js +++ b/integration_tests/helpers/walletClient.js @@ -156,6 +156,10 @@ class WalletClient { return await this.client.claimShaAtomicSwapTransaction(args); } + async claimHtlcRefund(args) { + return await this.client.ClaimHtlcRefundTransaction(args); + } + async importUtxos(outputs) { return await this.client.importUtxos({ outputs: outputs,