From 28a8f8b541f96d2bee4bd7f46cc1625dfeb0d323 Mon Sep 17 00:00:00 2001 From: "C.Lee Taylor" <47312074+leet4tari@users.noreply.github.com> Date: Thu, 23 Jun 2022 11:50:43 +0200 Subject: [PATCH 1/2] feat(ci): build both x86/arm64 docker images from GHA (#4204) Description Improve docker image build, supporting build with both x86-64 and ARM64 images together. Motivation and Context Reduce docker user confusion as both x86-64 and arm64 combined. How Has This Been Tested? Pulled all images local and test Raspberry Pi Run some of the images on my local machine and Raspberry pi --- .github/workflows/launchpad_docker.yml | 127 ++++++++++++++++++------- 1 file changed, 93 insertions(+), 34 deletions(-) diff --git a/.github/workflows/launchpad_docker.yml b/.github/workflows/launchpad_docker.yml index 8873371945..7a77f2516f 100644 --- a/.github/workflows/launchpad_docker.yml +++ b/.github/workflows/launchpad_docker.yml @@ -1,9 +1,12 @@ +--- +name: Build launchpad docker images + on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" - # branches: - # - development + branches: + - build-gha-docker-* workflow_dispatch: inputs: docker_tag: @@ -11,8 +14,6 @@ on: required: true default: "development" -name: Build launchpad docker images - env: toolchain: nightly-2021-11-20 CARGO_HTTP_MULTIPLEXING: false @@ -26,69 +27,127 @@ jobs: matrix: image_name: [ + frontail, monerod, - tari_base_node, - tari_console_wallet, - tari_mm_proxy, - tari_sha3_miner, tor, xmrig, ] + include: + - image_name: tari_base_node + app_name: base_node + app_exec: tari_base_node + - image_name: tari_wallet + app_name: wallet + app_exec: tari_console_wallet + - image_name: tari_mm_proxy + app_name: mm_proxy + app_exec: tari_merge_mining_proxy + - image_name: tari_sha3_miner + app_name: sha3_miner + app_exec: tari_miner + runs-on: ubuntu-latest + steps: - name: checkout uses: actions/checkout@v3 + with: + submodules: recursive - name: set env + id: environments run: | TAG="" - REF=${{github.ref}} if [ "${{ startsWith(github.ref, 'refs/tags/v') }}" == "true" ] then + REF=${{github.ref}} TAG="${REF/refs\/tags\//}" echo "docker tag from git: $TAG" + else + # Pull App version from file + VAPP=$(awk -F ' = ' \ + '$1 ~ /version/ { gsub(/["]/, "", $2); printf("%s",$2) }' \ + "${GITHUB_WORKSPACE}/applications/tari_base_node/Cargo.toml") + + VBRANCH=$(echo ${GITHUB_REF#refs/heads/}) + VSHA_SHORT=$(git rev-parse --short HEAD) + + TAG="v${VAPP}_${VBRANCH}_$(date -u '+%Y%m%d')_${VSHA_SHORT}" + echo "docker tag from App Version _ git branch _ \ + date stamp _ git short hash: ${TAG}" fi + echo "event name: ${{ github.event_name }}" if [ "${{ github.event_name }}" == "workflow_dispatch" ] then TAG="${{ github.event.inputs.docker_tag }}" echo "docker tag from workflow dispatch: $TAG" fi - echo "TAG=$TAG" >> $GITHUB_ENV IMAGE=${{ matrix.image_name }} echo "image: $IMAGE" - TEMP=${IMAGE/tari_/} - # echo "temp: $TEMP" - SERVICE="${TEMP/console_/}" - - echo "service: $SERVICE" - echo "SERVICE=$SERVICE" >> $GITHUB_ENV - - name: build docker image - run: | - if [ -z $SERVICE ] + # Setup dockerfile to use + if [ "${IMAGE:0:5}" == "tari_" ] then - echo "service is undefined!" - exit 1 + echo ::set-output name=dockerfile::tarilabs.Dockerfile + # Strip tari_ + IMAGE=${IMAGE/tari_/} + # Strip console_ + IMAGE=${IMAGE/console_/} + echo ::set-output name=app_name::${IMAGE} + echo ::set-output name=dockercontext::./ + else + DOCKERFILE=${IMAGE}.Dockerfile + DOCKERCONTEXT=./applications/launchpad/docker_rig/ + + # Pull the docker image TAG from dockerfile + SUBTAG=$(awk -F '=' '/ARG .*_VERSION=/ \ + { gsub(/["]/, "", $2); printf("%s",$2) }' \ + "${GITHUB_WORKSPACE}/${DOCKERCONTEXT}${DOCKERFILE}") + + echo ::set-output name=dockerfile::${DOCKERFILE} + echo ::set-output name=dockercontext::${DOCKERCONTEXT} + + if [ ! -z "${SUBTAG}" ] + then + TAG="${SUBTAG}_${TAG}" + echo "Adding subtag: ${TAG}" + fi + fi - cd applications/launchpad/docker_rig - docker-compose build $SERVICE - - name: Login to Quay.io + # Set docker image tag + echo "tag: ${TAG}" + echo ::set-output name=tag::$TAG + + - name: Login to Docker Image Provider uses: docker/login-action@v1 with: - registry: quay.io + registry: ${{ secrets.DOCKER_PROVIDER }} username: ${{ secrets.QUAY_USERNAME }} password: ${{ secrets.QUAY_ROBOT_TOKEN }} - - name: tag and push image - run: | - echo "tag: $TAG" - if [ -n "$TAG" ] - then - docker tag quay.io/tarilabs/${{ matrix.image_name }}:latest quay.io/tarilabs/${{ matrix.image_name }}:$TAG + - name: Set up QEMU for Docker + uses: docker/setup-qemu-action@v2 - docker push quay.io/tarilabs/${{ matrix.image_name }}:latest - docker push quay.io/tarilabs/${{ matrix.image_name }}:$TAG - fi + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Docker image build and push + uses: docker/build-push-action@v3 + with: + context: ${{ steps.environments.outputs.dockercontext }} + file: ./applications/launchpad/docker_rig/${{ steps.environments.outputs.dockerfile }} + platforms: linux/arm64, linux/amd64 + push: true + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=${{ steps.environments.outputs.tag }} + APP_NAME=${{ matrix.app_name }} + APP_EXEC=${{ matrix.app_exec }} + tags: | + ${{ secrets.DOCKER_PROVIDER }}/${{ secrets.DOCKER_REPO }}/${{ matrix.image_name }}:latest + ${{ secrets.DOCKER_PROVIDER }}/${{ secrets.DOCKER_REPO }}/${{ matrix.image_name }}:${{ steps.environments.outputs.tag }} From f2a7e1846341a69ddea6eb3541467e82e1bf2e47 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Thu, 23 Jun 2022 13:15:21 +0200 Subject: [PATCH 2/2] feat(wallet): allow UTXO selection by specific outputs and by token (#4227) Description --- - adds UtxoSelectionCriteria param to select_utxos - removes unique_id and parent_public_key utxo fetching - adds `UtxoSelectionCriteria::TokenOutputs` to allow db-level filtering for unique_id - adds `UtxoSelectionCriteria::SpecificOutputs` to allow spendin specific utxos - always sort (secondary to first sort) from most to least mature if tip_height is not known - remove some commented out and deprecated logic - remove MaturityThenSmallest ordering Motivation and Context --- The previous logic of UTXO selection has been kept equivalent (no utxo selection tests needed to be changed), but extended to allow Tokens and specific UTXOs to be selected at the db-level - Aurora wallet will need to spend specific utxos. - MaturityThenSmallest is redundant because it is only applicable if the tip height is not known, which is now handled independently of the ordering. i.e. if you dont know the tip height (pretty rare) you always want to select the most mature utxos first to reduce chances of it not being spendable regardless of selected value ordering - `UtxoSelectionCriteria::TokenOutputs` does db-level querying which is more performant, and will be chaned on development branch to ContractOutputs (so was worth doing) How Has This Been Tested? --- Existing tests for coin split and utxo selection Manually, running soin split and make it rain --- .../output_manager_service/input_selection.rs | 126 +++++++++++++++ .../wallet/src/output_manager_service/mod.rs | 25 +-- .../src/output_manager_service/service.rs | 148 +++++------------- .../storage/database/backend.rs | 5 +- .../storage/database/mod.rs | 7 +- .../storage/sqlite_db/mod.rs | 11 +- .../storage/sqlite_db/output_sql.rs | 105 ++++++++----- 7 files changed, 255 insertions(+), 172 deletions(-) create mode 100644 base_layer/wallet/src/output_manager_service/input_selection.rs diff --git a/base_layer/wallet/src/output_manager_service/input_selection.rs b/base_layer/wallet/src/output_manager_service/input_selection.rs new file mode 100644 index 0000000000..026ebe5616 --- /dev/null +++ b/base_layer/wallet/src/output_manager_service/input_selection.rs @@ -0,0 +1,126 @@ +// Copyright 2022. 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. + +use std::{ + fmt, + fmt::{Display, Formatter}, +}; + +use tari_common_types::types::PublicKey; + +use crate::output_manager_service::storage::models::DbUnblindedOutput; + +#[derive(Debug, Clone, Default)] +pub struct UtxoSelectionCriteria { + pub filter: UtxoSelectionFilter, + pub ordering: UtxoSelectionOrdering, +} + +impl UtxoSelectionCriteria { + pub fn largest_first() -> Self { + Self { + filter: UtxoSelectionFilter::Standard, + ordering: UtxoSelectionOrdering::LargestFirst, + } + } + + pub fn for_token(unique_id: Vec, parent_public_key: Option) -> Self { + Self { + filter: UtxoSelectionFilter::TokenOutput { + unique_id, + parent_public_key, + }, + ordering: UtxoSelectionOrdering::Default, + } + } +} + +impl Display for UtxoSelectionCriteria { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "filter: {}, ordering: {}", self.filter, self.ordering) + } +} + +/// UTXO selection ordering +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UtxoSelectionOrdering { + /// The Default ordering is heuristic and depends on the requested value and the value of the available UTXOs. + /// If the requested value is larger than the largest available UTXO, we select LargerFirst as inputs, otherwise + /// SmallestFirst. + Default, + /// 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 + SmallestFirst, + /// A strategy that selects the largest UTXOs first. Preferred when the amount is large + LargestFirst, +} + +impl Default for UtxoSelectionOrdering { + fn default() -> Self { + UtxoSelectionOrdering::Default + } +} + +impl Display for UtxoSelectionOrdering { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UtxoSelectionOrdering::SmallestFirst => write!(f, "Smallest"), + UtxoSelectionOrdering::LargestFirst => write!(f, "Largest"), + UtxoSelectionOrdering::Default => write!(f, "Default"), + } + } +} + +#[derive(Debug, Clone)] +pub enum UtxoSelectionFilter { + /// Select OutputType::Standard or OutputType::Coinbase outputs only + Standard, + /// Select matching token outputs. This will be deprecated in future. + TokenOutput { + unique_id: Vec, + parent_public_key: Option, + }, + /// Selects specific outputs. All outputs must be exist and be spendable. + SpecificOutputs { outputs: Vec }, +} + +impl Default for UtxoSelectionFilter { + fn default() -> Self { + UtxoSelectionFilter::Standard + } +} + +impl Display for UtxoSelectionFilter { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + UtxoSelectionFilter::Standard => { + write!(f, "Standard") + }, + UtxoSelectionFilter::TokenOutput { .. } => { + write!(f, "TokenOutput{{..}}") + }, + UtxoSelectionFilter::SpecificOutputs { outputs } => { + write!(f, "Specific({} output(s))", outputs.len()) + }, + } + } +} diff --git a/base_layer/wallet/src/output_manager_service/mod.rs b/base_layer/wallet/src/output_manager_service/mod.rs index 6b0238ac47..14f9a7b549 100644 --- a/base_layer/wallet/src/output_manager_service/mod.rs +++ b/base_layer/wallet/src/output_manager_service/mod.rs @@ -20,7 +20,20 @@ // 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::sync::Arc; +pub mod config; +pub mod error; +pub mod handle; + +mod input_selection; +pub use input_selection::{UtxoSelectionCriteria, UtxoSelectionFilter, UtxoSelectionOrdering}; + +mod recovery; +pub mod resources; +pub mod service; +pub mod storage; +mod tasks; + +use std::{marker::PhantomData, sync::Arc}; use futures::future; use log::*; @@ -47,16 +60,6 @@ use crate::{ }, }; -pub mod config; -pub mod error; -pub mod handle; -mod recovery; -pub mod resources; -pub mod service; -pub mod storage; -mod tasks; -use std::marker::PhantomData; - const LOG_TARGET: &str = "wallet::output_manager_service::initializer"; pub struct OutputManagerServiceInitializer diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 35e8b90b88..772b6ae135 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -20,7 +20,7 @@ // 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::{convert::TryInto, fmt, fmt::Display, sync::Arc}; +use std::{convert::TryInto, fmt, sync::Arc}; use blake2::Digest; use diesel::result::{DatabaseErrorKind, Error as DieselError}; @@ -80,6 +80,7 @@ use crate::{ PublicRewindKeys, RecoveredOutput, }, + input_selection::UtxoSelectionCriteria, recovery::StandardUtxoRecoverer, resources::{OutputManagerKeyManagerBranch, OutputManagerResources}, storage::{ @@ -840,9 +841,7 @@ where fee_per_gram, num_outputs, metadata_byte_size * num_outputs, - None, - None, - None, + UtxoSelectionCriteria::default(), ) .await?; @@ -887,16 +886,15 @@ where recipient_covenant.consensus_encode_exact_size(), ); + // TODO: Some(unique_id) means select the unique_id AND use the features of UTXOs with the unique_id. These + // should be able to be specified independently. + let selection_criteria = match unique_id.as_ref() { + Some(unique_id) => UtxoSelectionCriteria::for_token(unique_id.clone(), parent_public_key.as_ref().cloned()), + None => UtxoSelectionCriteria::default(), + }; + let input_selection = self - .select_utxos( - amount, - fee_per_gram, - 1, - metadata_byte_size, - None, - unique_id.as_ref(), - parent_public_key.as_ref(), - ) + .select_utxos(amount, fee_per_gram, 1, metadata_byte_size, selection_criteria) .await?; // TODO: improve this logic #LOGGED @@ -1100,15 +1098,19 @@ where .consensus_encode_exact_size() }) }); + + let selection_criteria = match spending_unique_id { + Some(unique_id) => UtxoSelectionCriteria::for_token(unique_id.clone(), spending_parent_public_key.cloned()), + None => UtxoSelectionCriteria::default(), + }; + let input_selection = self .select_utxos( total_value, fee_per_gram, outputs.len(), metadata_byte_size, - None, - spending_unique_id, - spending_parent_public_key, + selection_criteria, ) .await?; let offset = PrivateKey::random(&mut OsRng); @@ -1265,16 +1267,13 @@ where covenant.consensus_encode_exact_size(), ); + let selection_criteria = match unique_id { + Some(ref unique_id) => UtxoSelectionCriteria::for_token(unique_id.clone(), parent_public_key), + None => UtxoSelectionCriteria::default(), + }; + let input_selection = self - .select_utxos( - amount, - fee_per_gram, - 1, - metadata_byte_size, - None, - unique_id.as_ref(), - parent_public_key.as_ref(), - ) + .select_utxos(amount, fee_per_gram, 1, metadata_byte_size, selection_criteria) .await?; let offset = PrivateKey::random(&mut OsRng); @@ -1426,47 +1425,23 @@ where /// Select which unspent transaction outputs to use to send a transaction of the specified amount. Use the specified /// selection strategy to choose the outputs. It also determines if a change output is required. - #[allow(clippy::too_many_lines)] async fn select_utxos( &mut self, amount: MicroTari, fee_per_gram: MicroTari, num_outputs: usize, output_metadata_byte_size: usize, - strategy: Option, - unique_id: Option<&Vec>, - parent_public_key: Option<&PublicKey>, + selection_criteria: UtxoSelectionCriteria, ) -> Result { - let token = match unique_id { - Some(unique_id) => { - debug!(target: LOG_TARGET, "Looking for {:?}", unique_id); - // todo: new method to fetch by unique asset id - let uo = self.resources.db.fetch_all_unspent_outputs()?; - if let Some(token_id) = uo.into_iter().find(|x| match &x.unblinded_output.features.unique_id { - Some(token_unique_id) => { - debug!(target: LOG_TARGET, "Comparing with {:?}", token_unique_id); - token_unique_id == unique_id && - x.unblinded_output.features.parent_public_key.as_ref() == parent_public_key - }, - _ => false, - }) { - Some(token_id) - } else { - return Err(OutputManagerError::TokenUniqueIdNotFound); - } - }, - _ => None, - }; debug!( target: LOG_TARGET, - "select_utxos amount: {}, token : {:?}, fee_per_gram: {}, num_outputs: {}, output_metadata_byte_size: {}, \ - strategy: {:?}", + "select_utxos amount: {}, fee_per_gram: {}, num_outputs: {}, output_metadata_byte_size: {}, \ + selection_criteria: {:?}", amount, - token, fee_per_gram, num_outputs, output_metadata_byte_size, - strategy + selection_criteria ); let mut utxos = Vec::new(); @@ -1474,44 +1449,19 @@ where let mut fee_without_change = MicroTari::from(0); let mut fee_with_change = MicroTari::from(0); let fee_calc = self.get_fee_calc(); - if let Some(token) = token { - utxos_total_value = token.unblinded_output.value; - utxos.push(token); - } // 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, 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), _) => 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 - // } - // }, - // }; - warn!(target: LOG_TARGET, "select_utxos selection strategy: {}", strategy); + warn!( + target: LOG_TARGET, + "select_utxos selection criteria: {}", selection_criteria + ); + let tip_height = chain_metadata.as_ref().map(|m| m.height_of_longest_chain()); let uo = self .resources .db - .fetch_unspent_outputs_for_spending(strategy, amount, tip_height)?; + .fetch_unspent_outputs_for_spending(selection_criteria, amount, tip_height)?; trace!(target: LOG_TARGET, "We found {} UTXOs to select from", uo.len()); // Assumes that default Outputfeatures are used for change utxo @@ -1619,9 +1569,7 @@ where fee_per_gram, output_count, output_count * metadata_byte_size, - Some(UTXOSelectionStrategy::Largest), - None, - None, + UtxoSelectionCriteria::largest_first(), ) .await?; @@ -2080,32 +2028,6 @@ where } } -/// Different UTXO selection strategies for choosing which UTXO's are used to fulfill a transaction -#[derive(Debug, PartialEq, Eq)] -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 - Smallest, - // Start from oldest maturity to reduce the likelihood of grabbing locked up UTXOs - 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 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - UTXOSelectionStrategy::Smallest => write!(f, "Smallest"), - UTXOSelectionStrategy::MaturityThenSmallest => write!(f, "MaturityThenSmallest"), - UTXOSelectionStrategy::Largest => write!(f, "Largest"), - UTXOSelectionStrategy::Default => write!(f, "Default"), - } - } -} - /// This struct holds the detailed balance of the Output Manager Service. #[derive(Debug, Clone, PartialEq)] pub struct Balance { diff --git a/base_layer/wallet/src/output_manager_service/storage/database/backend.rs b/base_layer/wallet/src/output_manager_service/storage/database/backend.rs index 69e74f001b..add101d77e 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database/backend.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database/backend.rs @@ -10,7 +10,8 @@ use tari_core::transactions::transaction_components::{OutputFlags, TransactionOu use crate::output_manager_service::{ error::OutputManagerStorageError, - service::{Balance, UTXOSelectionStrategy}, + input_selection::UtxoSelectionCriteria, + service::Balance, storage::{ database::{DbKey, DbValue, OutputBackendQuery, WriteOperation}, models::DbUnblindedOutput, @@ -109,7 +110,7 @@ pub trait OutputManagerBackend: Send + Sync + Clone { fn add_unvalidated_output(&self, output: DbUnblindedOutput, tx_id: TxId) -> Result<(), OutputManagerStorageError>; fn fetch_unspent_outputs_for_spending( &self, - strategy: UTXOSelectionStrategy, + selection_criteria: UtxoSelectionCriteria, amount: u64, current_tip_height: Option, ) -> Result, OutputManagerStorageError>; diff --git a/base_layer/wallet/src/output_manager_service/storage/database/mod.rs b/base_layer/wallet/src/output_manager_service/storage/database/mod.rs index a550d61048..0e55dafacd 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database/mod.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database/mod.rs @@ -41,7 +41,8 @@ use tari_utilities::hex::Hex; use crate::output_manager_service::{ error::OutputManagerStorageError, - service::{Balance, UTXOSelectionStrategy}, + input_selection::UtxoSelectionCriteria, + service::Balance, storage::{ models::{DbUnblindedOutput, KnownOneSidedPaymentScript}, OutputStatus, @@ -250,13 +251,13 @@ where T: OutputManagerBackend + 'static /// Retrieves UTXOs than can be spent, sorted by priority, then value from smallest to largest. pub fn fetch_unspent_outputs_for_spending( &self, - strategy: UTXOSelectionStrategy, + selection_criteria: UtxoSelectionCriteria, amount: MicroTari, tip_height: Option, ) -> Result, OutputManagerStorageError> { let utxos = self .db - .fetch_unspent_outputs_for_spending(strategy, amount.as_u64(), tip_height)?; + .fetch_unspent_outputs_for_spending(selection_criteria, amount.as_u64(), tip_height)?; Ok(utxos) } 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 31cf388ab3..1b73c7160d 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 @@ -43,12 +43,13 @@ use tokio::time::Instant; use crate::{ output_manager_service::{ error::OutputManagerStorageError, - service::{Balance, UTXOSelectionStrategy}, + service::Balance, storage::{ database::{DbKey, DbKeyValuePair, DbValue, OutputBackendQuery, OutputManagerBackend, WriteOperation}, models::{DbUnblindedOutput, KnownOneSidedPaymentScript}, OutputStatus, }, + UtxoSelectionCriteria, }, schema::{known_one_sided_payment_scripts, outputs, outputs::columns}, storage::sqlite_utilities::wallet_db_connection::WalletDbConnection, @@ -1188,18 +1189,14 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { /// Retrieves UTXOs than can be spent, sorted by priority, then value from smallest to largest. fn fetch_unspent_outputs_for_spending( &self, - strategy: UTXOSelectionStrategy, + selection_criteria: UtxoSelectionCriteria, 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)?; + let mut outputs = OutputSql::fetch_unspent_outputs_for_spending(selection_criteria, amount, tip_height, &conn)?; for o in &mut outputs { self.decrypt_if_necessary(o)?; } 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 53e4b90c41..8ea2a6103b 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 @@ -45,13 +45,16 @@ use tari_utilities::hash::Hashable; use crate::{ output_manager_service::{ error::OutputManagerStorageError, - service::{Balance, UTXOSelectionStrategy}, + input_selection::UtxoSelectionCriteria, + service::Balance, storage::{ database::{OutputBackendQuery, SortDirection}, models::DbUnblindedOutput, sqlite_db::{UpdateOutput, UpdateOutputSql}, OutputStatus, }, + UtxoSelectionFilter, + UtxoSelectionOrdering, }, schema::outputs, util::{ @@ -178,54 +181,84 @@ impl OutputSql { /// Retrieves UTXOs than can be spent, sorted by priority, then value from smallest to largest. #[allow(clippy::cast_sign_loss)] pub fn fetch_unspent_outputs_for_spending( - mut strategy: UTXOSelectionStrategy, + selection_criteria: UtxoSelectionCriteria, amount: u64, - tip_height: i64, + tip_height: Option, 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)) - .filter(outputs::features_unique_id.is_null()) - .filter(outputs::features_parent_public_key.is_null()) - .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)) - .filter(outputs::features_unique_id.is_null()) - .filter(outputs::features_parent_public_key.is_null()) .order_by(outputs::spending_priority.desc()); - match strategy { - UTXOSelectionStrategy::Smallest => { - query = query.then_order_by(outputs::value.asc()); + + match selection_criteria.filter { + UtxoSelectionFilter::Standard => { + query = query + .filter(outputs::features_unique_id.is_null()) + .filter(outputs::features_parent_public_key.is_null()); }, - UTXOSelectionStrategy::MaturityThenSmallest => { + UtxoSelectionFilter::TokenOutput { + parent_public_key, + unique_id, + } => { query = query - .then_order_by(outputs::maturity.asc()) - .then_order_by(outputs::value.asc()); + .filter(outputs::features_unique_id.eq(unique_id)) + .filter(outputs::features_parent_public_key.eq(parent_public_key.as_ref().map(|pk| pk.to_vec()))); + }, + UtxoSelectionFilter::SpecificOutputs { outputs } => { + query = query.filter(outputs::hash.eq_any(outputs.into_iter().map(|o| o.hash))) + }, + } + + match selection_criteria.ordering { + UtxoSelectionOrdering::SmallestFirst => { + query = query.then_order_by(outputs::value.asc()); }, - UTXOSelectionStrategy::Largest => { + UtxoSelectionOrdering::LargestFirst => { query = query.then_order_by(outputs::value.desc()); }, - UTXOSelectionStrategy::Default => {}, + UtxoSelectionOrdering::Default => { + let i64_tip_height = tip_height.and_then(|h| i64::try_from(h).ok()).unwrap_or(i64::MAX); + // lets get the max value for all utxos + let max: Option = outputs::table + .filter(outputs::status.eq(OutputStatus::Unspent as i32)) + .filter(outputs::script_lock_height.le(i64_tip_height)) + .filter(outputs::maturity.le(i64_tip_height)) + .filter(outputs::features_unique_id.is_null()) + .filter(outputs::features_parent_public_key.is_null()) + .order(outputs::value.desc()) + .select(outputs::value) + .first(conn) + .optional()?; + match max { + Some(max) if amount > max as u64 => { + // Want to reduce the number of inputs to reduce fees + query = query.then_order_by(outputs::value.desc()); + }, + Some(_) => { + // Use the smaller utxos to make up this transaction. + query = query.then_order_by(outputs::value.asc()); + }, + None => { + // No spendable UTXOs? + query = query.then_order_by(outputs::value.asc()); + }, + } + }, }; + match tip_height { + Some(tip_height) => { + let i64_tip_height = i64::try_from(tip_height).unwrap_or(i64::MAX); + query = query + .filter(outputs::script_lock_height.le(i64_tip_height)) + .filter(outputs::maturity.le(i64_tip_height)); + }, + None => { + // If we don't know the current tip height, order by maturity ASC to reduce the chances of a locked + // output being used. + query = query.then_order_by(outputs::maturity.asc()); + }, + } Ok(query.load(conn)?) }