From 21c5eec5f6ef004266957e11fdf8b21eff5f3055 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Tue, 2 Apr 2019 21:05:13 -0500 Subject: [PATCH 1/2] Remove referralIdInputTextField The referralId support did not get any interest so we removed it from the UI. It is still supported via prog argument and can be re-enabled again once there are use cases (API?). I left it commented out to make re-enabling easier. --- .../settings/preferences/PreferencesView.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java index 87104589f67..685f931d766 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java @@ -42,7 +42,6 @@ import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.provider.fee.FeeService; -import bisq.core.trade.statistics.ReferralIdService; import bisq.core.user.BlockChainExplorer; import bisq.core.user.Preferences; import bisq.core.util.BSFormatter; @@ -103,7 +102,8 @@ public class PreferencesView extends ActivatableViewAndModel transactionFeeFocusedListener; private final Preferences preferences; private final FeeService feeService; - private final ReferralIdService referralIdService; + //private final ReferralIdService referralIdService; private final AssetService assetService; private final FilterManager filterManager; private final DaoFacade daoFacade; @@ -144,12 +144,10 @@ public class PreferencesView extends ActivatableViewAndModel { if (!newValue.equals(oldValue)) referralIdService.setReferralId(newValue); - }; + };*/ // AvoidStandbyModeService avoidStandbyMode = addSlideToggleButton(root, ++gridRow, @@ -608,8 +606,8 @@ private void activateGeneralOptions() { transactionFeeInputTextField.setText(String.valueOf(getTxFeeForWithdrawalPerByte())); ignoreTradersListInputTextField.setText(String.join(", ", preferences.getIgnoreTradersList())); - referralIdService.getOptionalReferralId().ifPresent(referralId -> referralIdInputTextField.setText(referralId)); - referralIdInputTextField.setPromptText(Res.get("setting.preferences.refererId.prompt")); + /* referralIdService.getOptionalReferralId().ifPresent(referralId -> referralIdInputTextField.setText(referralId)); + referralIdInputTextField.setPromptText(Res.get("setting.preferences.refererId.prompt"));*/ userLanguageComboBox.setItems(languageCodes); userLanguageComboBox.getSelectionModel().select(preferences.getUserLanguage()); userLanguageComboBox.setConverter(new StringConverter<>() { @@ -694,7 +692,7 @@ public BlockChainExplorer fromString(String string) { transactionFeeInputTextField.focusedProperty().addListener(transactionFeeFocusedListener); ignoreTradersListInputTextField.textProperty().addListener(ignoreTradersListListener); useCustomFee.selectedProperty().addListener(useCustomFeeCheckboxListener); - referralIdInputTextField.textProperty().addListener(referralIdListener); + //referralIdInputTextField.textProperty().addListener(referralIdListener); } private Coin getTxFeeForWithdrawalPerByte() { @@ -858,7 +856,7 @@ private void deactivateGeneralOptions() { feeService.feeUpdateCounterProperty().removeListener(transactionFeeChangeListener); ignoreTradersListInputTextField.textProperty().removeListener(ignoreTradersListListener); useCustomFee.selectedProperty().removeListener(useCustomFeeCheckboxListener); - referralIdInputTextField.textProperty().removeListener(referralIdListener); + //referralIdInputTextField.textProperty().removeListener(referralIdListener); } private void deactivateDisplayCurrencies() { From fcbc3b659b040ed6aa6b91b4a23f3adeedb3048a Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Tue, 2 Apr 2019 22:22:42 -0500 Subject: [PATCH 2/2] Ignore utxo below a user defined threshold to avoid dust attacks Implements https://github.com/bisq-network/bisq/issues/2604 --- common/src/main/proto/pb.proto | 1 + .../btc/wallet/BisqDefaultCoinSelector.java | 26 +++++---- .../bisq/core/btc/wallet/BsqCoinSelector.java | 7 +++ .../core/btc/wallet/BsqWalletService.java | 7 +++ .../bisq/core/btc/wallet/BtcCoinSelector.java | 30 ++++++---- .../core/btc/wallet/BtcWalletService.java | 56 ++++++++++++------- .../core/btc/wallet/NonBsqCoinSelector.java | 6 ++ .../core/btc/wallet/TradeWalletService.java | 27 +++++---- .../bisq/core/btc/wallet/WalletService.java | 12 ++-- .../main/java/bisq/core/user/Preferences.java | 11 ++++ .../bisq/core/user/PreferencesPayload.java | 7 ++- .../resources/i18n/displayStrings.properties | 8 +++ .../TransactionListItemFactory.java | 8 ++- .../transactions/TransactionsListItem.java | 12 +++- .../funds/transactions/TransactionsView.java | 23 +++++--- .../settings/preferences/PreferencesView.java | 36 +++++++++++- 16 files changed, 204 insertions(+), 73 deletions(-) diff --git a/common/src/main/proto/pb.proto b/common/src/main/proto/pb.proto index f4908b804c2..20d52f1d63d 100644 --- a/common/src/main/proto/pb.proto +++ b/common/src/main/proto/pb.proto @@ -1351,6 +1351,7 @@ message PreferencesPayload { string rpc_pw = 48; string take_offer_selected_payment_account_id = 49; double buyer_security_deposit_as_percent = 50; + int32 ignore_dust_threshold = 51; } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/btc/wallet/BisqDefaultCoinSelector.java b/core/src/main/java/bisq/core/btc/wallet/BisqDefaultCoinSelector.java index 40a54eef2a6..57923d0ab32 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BisqDefaultCoinSelector.java +++ b/core/src/main/java/bisq/core/btc/wallet/BisqDefaultCoinSelector.java @@ -75,17 +75,19 @@ public CoinSelection select(Coin target, List candidates) { long total = 0; long targetValue = target.value; for (TransactionOutput output : sortedOutputs) { - if (total >= targetValue) { - long change = total - targetValue; - if (change == 0 || change >= Restrictions.getMinNonDustOutput().value) - break; - } - - if (output.getParentTransaction() != null && - isTxSpendable(output.getParentTransaction()) && - isTxOutputSpendable(output)) { - selected.add(output); - total += output.getValue().value; + if (!isDustAttackUtxo(output)) { + if (total >= targetValue) { + long change = total - targetValue; + if (change == 0 || change >= Restrictions.getMinNonDustOutput().value) + break; + } + + if (output.getParentTransaction() != null && + isTxSpendable(output.getParentTransaction()) && + isTxOutputSpendable(output)) { + selected.add(output); + total += output.getValue().value; + } } } // Total may be lower than target here, if the given candidates were insufficient to create to requested @@ -93,6 +95,8 @@ public CoinSelection select(Coin target, List candidates) { return new CoinSelection(Coin.valueOf(total), selected); } + protected abstract boolean isDustAttackUtxo(TransactionOutput output); + public Coin getChange(Coin target, CoinSelection coinSelection) throws InsufficientMoneyException { long value = target.value; long available = coinSelection.valueGathered.value; diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqCoinSelector.java b/core/src/main/java/bisq/core/btc/wallet/BsqCoinSelector.java index f60db586368..72e3b1af347 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BsqCoinSelector.java +++ b/core/src/main/java/bisq/core/btc/wallet/BsqCoinSelector.java @@ -65,4 +65,11 @@ protected boolean isTxOutputSpendable(TransactionOutput output) { // check if it is an own change output. return unconfirmedBsqChangeOutputListService.hasTransactionOutput(output); } + + // For BSQ we do not check for dust attack utxos as they are 5.46 BSQ and a considerable value. + // The default 546 sat dust limit is handled in the BitcoinJ side anyway. + @Override + protected boolean isDustAttackUtxo(TransactionOutput output) { + return false; + } } diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java index 30b3fe4b456..43fea1bfb77 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java @@ -669,4 +669,11 @@ public Address getUnusedAddress() { public String getUnusedBsqAddressAsString() { return "B" + getUnusedAddress().toBase58(); } + + // For BSQ we do not check for dust attack utxos as they are 5.46 BSQ and a considerable value. + // The default 546 sat dust limit is handled in the BitcoinJ side anyway. + @Override + protected boolean isDustAttackUtxo(TransactionOutput output) { + return false; + } } diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcCoinSelector.java b/core/src/main/java/bisq/core/btc/wallet/BtcCoinSelector.java index 8b765eeb6af..5eaf88d86fa 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcCoinSelector.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcCoinSelector.java @@ -24,38 +24,38 @@ import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; /** * We use a specialized version of the CoinSelector based on the DefaultCoinSelector implementation. * We lookup for spendable outputs which matches any of our addresses. */ +@Slf4j class BtcCoinSelector extends BisqDefaultCoinSelector { - private static final Logger log = LoggerFactory.getLogger(BtcCoinSelector.class); - private final Set
addresses; + private final long ignoreDustThreshold; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - BtcCoinSelector(Set
addresses, boolean permitForeignPendingTx) { + private BtcCoinSelector(Set
addresses, long ignoreDustThreshold, boolean permitForeignPendingTx) { super(permitForeignPendingTx); this.addresses = addresses; + this.ignoreDustThreshold = ignoreDustThreshold; } - BtcCoinSelector(Set
addresses) { - this(addresses, true); + BtcCoinSelector(Set
addresses, long ignoreDustThreshold) { + this(addresses, ignoreDustThreshold, true); } - BtcCoinSelector(Address address, @SuppressWarnings("SameParameterValue") boolean permitForeignPendingTx) { - this(Sets.newHashSet(address), permitForeignPendingTx); + BtcCoinSelector(Address address, long ignoreDustThreshold, @SuppressWarnings("SameParameterValue") boolean permitForeignPendingTx) { + this(Sets.newHashSet(address), ignoreDustThreshold, permitForeignPendingTx); } - BtcCoinSelector(Address address) { - this(Sets.newHashSet(address), true); + BtcCoinSelector(Address address, long ignoreDustThreshold) { + this(Sets.newHashSet(address), ignoreDustThreshold, true); } @Override @@ -68,4 +68,12 @@ protected boolean isTxOutputSpendable(TransactionOutput output) { return false; } } + + // We ignore utxos which are considered dust attacks for spying on users wallets. + // The ignoreDustThreshold value is set in the preferences. If not set we use default non dust + // value of 546 sat. + @Override + protected boolean isDustAttackUtxo(TransactionOutput output) { + return output.getValue().value < ignoreDustThreshold; + } } diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index 92fdb953849..0e0ee6f0e9d 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -202,17 +202,18 @@ private Transaction completePreparedProposalTx(Transaction feeTx, byte[] opRetur // safety check counter to avoid endless loops int counter = 0; // estimated size of input sig - final int sigSizePerInput = 106; + int sigSizePerInput = 106; // typical size for a tx with 3 inputs int txSizeWithUnsignedInputs = 300; - final Coin txFeePerByte = feeService.getTxFeePerByte(); + Coin txFeePerByte = feeService.getTxFeePerByte(); Address changeAddress = getFreshAddressEntry().getAddress(); checkNotNull(changeAddress, "changeAddress must not be null"); - final BtcCoinSelector coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE)); - final List preparedBsqTxInputs = preparedTx.getInputs(); - final List preparedBsqTxOutputs = preparedTx.getOutputs(); + BtcCoinSelector coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); + List preparedBsqTxInputs = preparedTx.getInputs(); + List preparedBsqTxOutputs = preparedTx.getOutputs(); int numInputs = preparedBsqTxInputs.size(); Transaction resultTx = null; boolean isFeeOutsideTolerance; @@ -309,17 +310,18 @@ private Transaction addInputsForMinerFee(Transaction preparedTx, byte[] opReturn // safety check counter to avoid endless loops int counter = 0; // estimated size of input sig - final int sigSizePerInput = 106; + int sigSizePerInput = 106; // typical size for a tx with 3 inputs int txSizeWithUnsignedInputs = 300; - final Coin txFeePerByte = feeService.getTxFeePerByte(); + Coin txFeePerByte = feeService.getTxFeePerByte(); Address changeAddress = getFreshAddressEntry().getAddress(); checkNotNull(changeAddress, "changeAddress must not be null"); - final BtcCoinSelector coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE)); - final List preparedBsqTxInputs = preparedTx.getInputs(); - final List preparedBsqTxOutputs = preparedTx.getOutputs(); + BtcCoinSelector coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); + List preparedBsqTxInputs = preparedTx.getInputs(); + List preparedBsqTxOutputs = preparedTx.getOutputs(); int numInputs = preparedBsqTxInputs.size(); Transaction resultTx = null; boolean isFeeOutsideTolerance; @@ -444,20 +446,21 @@ public Transaction completePreparedBsqTx(Transaction preparedBsqTx, boolean useC // safety check counter to avoid endless loops int counter = 0; // estimated size of input sig - final int sigSizePerInput = 106; + int sigSizePerInput = 106; // typical size for a tx with 2 inputs int txSizeWithUnsignedInputs = 203; // If useCustomTxFee we allow overriding the estimated fee from preferences - final Coin txFeePerByte = useCustomTxFee ? getTxFeeForWithdrawalPerByte() : feeService.getTxFeePerByte(); + Coin txFeePerByte = useCustomTxFee ? getTxFeeForWithdrawalPerByte() : feeService.getTxFeePerByte(); // In case there are no change outputs we force a change by adding min dust to the BTC input Coin forcedChangeValue = Coin.ZERO; Address changeAddress = getFreshAddressEntry().getAddress(); checkNotNull(changeAddress, "changeAddress must not be null"); - final BtcCoinSelector coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE)); - final List preparedBsqTxInputs = preparedBsqTx.getInputs(); - final List preparedBsqTxOutputs = preparedBsqTx.getOutputs(); + BtcCoinSelector coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); + List preparedBsqTxInputs = preparedBsqTx.getInputs(); + List preparedBsqTxOutputs = preparedBsqTx.getOutputs(); int numInputs = preparedBsqTxInputs.size() + 1; // We add 1 for the BTC fee input Transaction resultTx = null; boolean isFeeOutsideTolerance; @@ -781,7 +784,7 @@ public void doubleSpendTransaction(String txId, Runnable resultHandler, ErrorMes sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.aesKey = aesKey; - sendRequest.coinSelector = new BtcCoinSelector(toAddress); + sendRequest.coinSelector = new BtcCoinSelector(toAddress, preferences.getIgnoreDustThreshold()); sendRequest.changeAddress = toAddress; wallet.completeTx(sendRequest); tx = sendRequest.tx; @@ -802,7 +805,7 @@ public void doubleSpendTransaction(String txId, Runnable resultHandler, ErrorMes sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.aesKey = aesKey; - sendRequest.coinSelector = new BtcCoinSelector(toAddress); + sendRequest.coinSelector = new BtcCoinSelector(toAddress, preferences.getIgnoreDustThreshold()); sendRequest.changeAddress = toAddress; sendResult = wallet.sendCoins(sendRequest); } catch (InsufficientMoneyException e) { @@ -818,7 +821,8 @@ public void doubleSpendTransaction(String txId, Runnable resultHandler, ErrorMes sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.aesKey = aesKey; - sendRequest.coinSelector = new BtcCoinSelector(toAddress, false); + sendRequest.coinSelector = new BtcCoinSelector(toAddress, + preferences.getIgnoreDustThreshold(), false); sendRequest.changeAddress = toAddress; try { @@ -972,7 +976,8 @@ public int getEstimatedFeeTxSize(List outputValues, Coin txFee) SendRequest sendRequest = SendRequest.forTx(transaction); sendRequest.shuffleOutputs = false; sendRequest.aesKey = aesKey; - sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE)); + sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); sendRequest.fee = txFee; sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; @@ -1044,7 +1049,7 @@ private SendRequest getSendRequest(String fromAddress, checkNotNull(addressEntry.get(), "addressEntry.get() must not be null"); checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must not be null"); - sendRequest.coinSelector = new BtcCoinSelector(addressEntry.get().getAddress()); + sendRequest.coinSelector = new BtcCoinSelector(addressEntry.get().getAddress(), preferences.getIgnoreDustThreshold()); sendRequest.changeAddress = addressEntry.get().getAddress(); return sendRequest; } @@ -1089,7 +1094,8 @@ private SendRequest getSendRequestForMultipleAddresses(Set fromAddresses if (addressEntries.isEmpty()) throw new AddressEntryException("No Addresses for withdraw found in our wallet"); - sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesFromAddressEntries(addressEntries)); + sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesFromAddressEntries(addressEntries), + preferences.getIgnoreDustThreshold()); Optional addressEntryOptional = Optional.empty(); AddressEntry changeAddressAddressEntry = null; if (changeAddress != null) @@ -1100,4 +1106,12 @@ private SendRequest getSendRequestForMultipleAddresses(Set fromAddresses sendRequest.changeAddress = changeAddressAddressEntry.getAddress(); return sendRequest; } + + // We ignore utxos which are considered dust attacks for spying on users wallets. + // The ignoreDustThreshold value is set in the preferences. If not set we use default non dust + // value of 546 sat. + @Override + protected boolean isDustAttackUtxo(TransactionOutput output) { + return output.getValue().value < preferences.getIgnoreDustThreshold(); + } } diff --git a/core/src/main/java/bisq/core/btc/wallet/NonBsqCoinSelector.java b/core/src/main/java/bisq/core/btc/wallet/NonBsqCoinSelector.java index 0b9b106317e..ae9bdb6eca5 100644 --- a/core/src/main/java/bisq/core/btc/wallet/NonBsqCoinSelector.java +++ b/core/src/main/java/bisq/core/btc/wallet/NonBsqCoinSelector.java @@ -59,4 +59,10 @@ protected boolean isTxOutputSpendable(TransactionOutput output) { // So we consider any txOutput which is not in the state as BTC output. return !daoStateService.existsTxOutput(key) || daoStateService.isRejectedIssuanceOutput(key); } + + // BTC utxo in the BSQ wallet are usually from rejected comp request so we don't expect dust attack utxos here. + @Override + protected boolean isDustAttackUtxo(TransactionOutput output) { + return false; + } } diff --git a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java index 9e3b2e2990f..3db2070e5d4 100644 --- a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java @@ -28,6 +28,7 @@ import bisq.core.btc.setup.WalletConfig; import bisq.core.btc.setup.WalletsSetup; import bisq.core.locale.Res; +import bisq.core.user.Preferences; import org.bitcoinj.core.Address; import org.bitcoinj.core.AddressFormatException; @@ -115,6 +116,7 @@ public class TradeWalletService { private static final Logger log = LoggerFactory.getLogger(TradeWalletService.class); private final WalletsSetup walletsSetup; + private final Preferences preferences; private final NetworkParameters params; @Nullable @@ -130,8 +132,9 @@ public class TradeWalletService { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public TradeWalletService(WalletsSetup walletsSetup) { + public TradeWalletService(WalletsSetup walletsSetup, Preferences preferences) { this.walletsSetup = walletsSetup; + this.preferences = preferences; this.params = BisqEnvironment.getParameters(); walletsSetup.addSetupCompletedHandler(() -> { walletConfig = walletsSetup.getWalletConfig(); @@ -198,10 +201,12 @@ public Transaction createBtcTradingFeeTx(Address fundingAddress, sendRequest = SendRequest.forTx(tradingFeeTx); sendRequest.shuffleOutputs = false; sendRequest.aesKey = aesKey; - if (useSavingsWallet) - sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE)); - else - sendRequest.coinSelector = new BtcCoinSelector(fundingAddress); + if (useSavingsWallet) { + sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); + } else { + sendRequest.coinSelector = new BtcCoinSelector(fundingAddress, preferences.getIgnoreDustThreshold()); + } // We use a fixed fee sendRequest.fee = txFee; @@ -278,10 +283,12 @@ public Transaction completeBsqTradingFeeTx(Transaction preparedBsqTx, SendRequest sendRequest = SendRequest.forTx(preparedBsqTx); sendRequest.shuffleOutputs = false; sendRequest.aesKey = aesKey; - if (useSavingsWallet) - sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE)); - else - sendRequest.coinSelector = new BtcCoinSelector(fundingAddress); + if (useSavingsWallet) { + sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); + } else { + sendRequest.coinSelector = new BtcCoinSelector(fundingAddress, preferences.getIgnoreDustThreshold()); + } // We use a fixed fee sendRequest.fee = txFee; sendRequest.feePerKb = Coin.ZERO; @@ -1196,7 +1203,7 @@ private void addAvailableInputsAndChangeOutputs(Transaction transaction, Address sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; // we allow spending of unconfirmed tx (double spend risk is low and usability would suffer if we need to wait for 1 confirmation) - sendRequest.coinSelector = new BtcCoinSelector(address); + sendRequest.coinSelector = new BtcCoinSelector(address, preferences.getIgnoreDustThreshold()); // We use always the same address in a trade for all transactions sendRequest.changeAddress = changeAddress; // With the usage of completeTx() we get all the work done with fee calculation, validation and coin selection. diff --git a/core/src/main/java/bisq/core/btc/wallet/WalletService.java b/core/src/main/java/bisq/core/btc/wallet/WalletService.java index a871d1c44a4..c5c7cec6b3f 100644 --- a/core/src/main/java/bisq/core/btc/wallet/WalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/WalletService.java @@ -423,14 +423,18 @@ public Coin getBalanceForAddress(Address address) { protected Coin getBalance(List transactionOutputs, Address address) { Coin balance = Coin.ZERO; for (TransactionOutput output : transactionOutputs) { - if (isOutputScriptConvertibleToAddress(output) && - address != null && - address.equals(getAddressFromOutput(output))) - balance = balance.add(output.getValue()); + if (!isDustAttackUtxo(output)) { + if (isOutputScriptConvertibleToAddress(output) && + address != null && + address.equals(getAddressFromOutput(output))) + balance = balance.add(output.getValue()); + } } return balance; } + protected abstract boolean isDustAttackUtxo(TransactionOutput output); + public Coin getBalance(TransactionOutput output) { return getBalanceForAddress(getAddressFromOutput(output)); } diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index 84d4a7c692d..e3c4c077b2a 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -288,6 +288,10 @@ else if (useTorFlagFromOptions.equals("true")) if (rpcPasswordFromOptions != null && !rpcPasswordFromOptions.isEmpty()) setRpcPw(rpcPasswordFromOptions); + if (prefPayload.getIgnoreDustThreshold() < Restrictions.getMinNonDustOutput().value) { + setIgnoreDustThreshold(600); + } + // For users from old versions the 4 flags a false but we want to have it true by default // PhoneKeyAndToken is also null so we can use that to enable the flags if (prefPayload.getPhoneKeyAndToken() == null) { @@ -618,6 +622,11 @@ public void setTakeOfferSelectedPaymentAccountId(String value) { persist(); } + public void setIgnoreDustThreshold(int value) { + prefPayload.setIgnoreDustThreshold(value); + persist(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Getter @@ -848,6 +857,8 @@ private interface ExcludesDelegateMethods { void setTakeOfferSelectedPaymentAccountId(String value); + void setIgnoreDustThreshold(int value); + void setBuyerSecurityDepositAsPercent(double buyerSecurityDepositAsPercent); double getBuyerSecurityDepositAsPercent(); diff --git a/core/src/main/java/bisq/core/user/PreferencesPayload.java b/core/src/main/java/bisq/core/user/PreferencesPayload.java index 9c62b20c815..b4fd96aa386 100644 --- a/core/src/main/java/bisq/core/user/PreferencesPayload.java +++ b/core/src/main/java/bisq/core/user/PreferencesPayload.java @@ -121,6 +121,7 @@ public final class PreferencesPayload implements PersistableEnvelope { @Nullable String takeOfferSelectedPaymentAccountId; private double buyerSecurityDepositAsPercent = Restrictions.getDefaultBuyerSecurityDepositAsPercent(); + private int ignoreDustThreshold = 600; /////////////////////////////////////////////////////////////////////////////////////////// @@ -177,7 +178,8 @@ public Message toProtoMessage() { .setUsePriceNotifications(usePriceNotifications) .setUseStandbyMode(useStandbyMode) .setIsDaoFullNode(isDaoFullNode) - .setBuyerSecurityDepositAsPercent(buyerSecurityDepositAsPercent); + .setBuyerSecurityDepositAsPercent(buyerSecurityDepositAsPercent). + setIgnoreDustThreshold(ignoreDustThreshold); Optional.ofNullable(backupDirectory).ifPresent(builder::setBackupDirectory); Optional.ofNullable(preferredTradeCurrency).ifPresent(e -> builder.setPreferredTradeCurrency((PB.TradeCurrency) e.toProtoMessage())); Optional.ofNullable(offerBookChartScreenCurrencyCode).ifPresent(builder::setOfferBookChartScreenCurrencyCode); @@ -259,6 +261,7 @@ public static PersistableEnvelope fromProto(PB.PreferencesPayload proto, CorePro proto.getRpcUser().isEmpty() ? null : proto.getRpcUser(), proto.getRpcPw().isEmpty() ? null : proto.getRpcPw(), proto.getTakeOfferSelectedPaymentAccountId().isEmpty() ? null : proto.getTakeOfferSelectedPaymentAccountId(), - proto.getBuyerSecurityDepositAsPercent()); + proto.getBuyerSecurityDepositAsPercent(), + proto.getIgnoreDustThreshold()); } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 0f261ee5d70..4824591ce9b 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -785,6 +785,13 @@ funds.tx.direction.self=Sent to yourself funds.tx.daoTxFee=Miner fee for DAO tx funds.tx.reimbursementRequestTxFee=Reimbursement request funds.tx.compensationRequestTxFee=Compensation request +funds.tx.dustAttackTx=Received dust +funds.tx.dustAttackTx.popup=This transaction is sending a very small BTC amount to your wallet and might be an attempt \ + from chain analysis companies to spy on your wallet.\n\n\ + If you use that transaction output in a spending transaction they will learn that you are likely the owner of the \ + other address as well (coin merge).\n\n\ + To protect your privacy the Bisq wallet ignores such dust outputs for spending purposes and in the balance display. \ + You can set the threshold amount when an output is considered dust in the settings. #################################################################### @@ -885,6 +892,7 @@ setting.preferences.txFeeMin=Transaction fee must be at least {0} satoshis/byte setting.preferences.txFeeTooLarge=Your input is above any reasonable value (>5000 satoshis/byte). Transaction fee is usually in the range of 50-400 satoshis/byte. setting.preferences.ignorePeers=Ignore peers with onion address (comma sep.) setting.preferences.refererId=Referral ID +setting.preferences.ignoreDustThreshold=Min. non-dust output value setting.preferences.refererId.prompt=Optional referral ID setting.preferences.currenciesInList=Currencies in market price feed list setting.preferences.prefCurrency=Preferred currency diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionListItemFactory.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionListItemFactory.java index a19d751a49b..752ca17e9e4 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionListItemFactory.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionListItemFactory.java @@ -21,6 +21,7 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.dao.DaoFacade; import bisq.core.trade.Tradable; +import bisq.core.user.Preferences; import bisq.core.util.BSFormatter; import org.bitcoinj.core.Transaction; @@ -36,20 +37,23 @@ public class TransactionListItemFactory { private final BsqWalletService bsqWalletService; private final DaoFacade daoFacade; private final BSFormatter formatter; + private final Preferences preferences; @Inject TransactionListItemFactory(BtcWalletService btcWalletService, BsqWalletService bsqWalletService, - DaoFacade daoFacade, BSFormatter formatter) { + DaoFacade daoFacade, BSFormatter formatter, Preferences preferences) { this.btcWalletService = btcWalletService; this.bsqWalletService = bsqWalletService; this.daoFacade = daoFacade; this.formatter = formatter; + this.preferences = preferences; } TransactionsListItem create(Transaction transaction, @Nullable TransactionAwareTradable tradable) { Optional maybeTradable = Optional.ofNullable(tradable) .map(TransactionAwareTradable::asTradable); - return new TransactionsListItem(transaction, btcWalletService, bsqWalletService, maybeTradable, daoFacade, formatter); + return new TransactionsListItem(transaction, btcWalletService, bsqWalletService, maybeTradable, + daoFacade, formatter, preferences.getIgnoreDustThreshold()); } } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java index 2464859dbb9..afd36b5861d 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java @@ -43,6 +43,7 @@ import java.util.Date; import java.util.Optional; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @@ -66,6 +67,8 @@ class TransactionsListItem { private boolean detailsAvailable; private Coin amountAsCoin = Coin.ZERO; private int confirmations = 0; + @Getter + private final boolean isDustAttackTx; // used at exportCSV TransactionsListItem() { @@ -75,6 +78,7 @@ class TransactionsListItem { tooltip = null; txId = null; formatter = null; + isDustAttackTx = false; } TransactionsListItem(Transaction transaction, @@ -82,7 +86,8 @@ class TransactionsListItem { BsqWalletService bsqWalletService, Optional tradableOptional, DaoFacade daoFacade, - BSFormatter formatter) { + BSFormatter formatter, + long ignoreDustThreshold) { this.btcWalletService = btcWalletService; this.formatter = formatter; @@ -235,6 +240,11 @@ else if (details.isEmpty()) // Use tx.getIncludedInBestChainAt() when available, otherwise use tx.getUpdateTime() date = transaction.getIncludedInBestChainAt() != null ? transaction.getIncludedInBestChainAt() : transaction.getUpdateTime(); dateString = formatter.formatDateTime(date); + + isDustAttackTx = received && valueSentToMe.value < ignoreDustThreshold; + if (isDustAttackTx) { + details = Res.get("funds.tx.dustAttackTx"); + } } public void cleanup() { diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java index 21260550d7e..fe3c3b904bc 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java @@ -341,7 +341,7 @@ public TableCell call(TableColumn column) { return new TableCell<>() { - private HyperlinkWithIcon field; + private HyperlinkWithIcon hyperlinkWithIcon; @Override public void updateItem(final TransactionsListItem item, boolean empty) { @@ -349,17 +349,24 @@ public void updateItem(final TransactionsListItem item, boolean empty) { if (item != null && !empty) { if (item.getDetailsAvailable()) { - field = new HyperlinkWithIcon(item.getDetails(), AwesomeIcon.INFO_SIGN); - field.setOnAction(event -> openDetailPopup(item)); - field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); - setGraphic(field); + hyperlinkWithIcon = new HyperlinkWithIcon(item.getDetails(), AwesomeIcon.INFO_SIGN); + hyperlinkWithIcon.setOnAction(event -> openDetailPopup(item)); + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); + setGraphic(hyperlinkWithIcon); + // If details are available its a trade tx and we don't expect any dust attack tx } else { - setGraphic(new AutoTooltipLabel(item.getDetails())); + if (item.isDustAttackTx()) { + hyperlinkWithIcon = new HyperlinkWithIcon(item.getDetails(), AwesomeIcon.WARNING_SIGN); + hyperlinkWithIcon.setOnAction(event -> new Popup<>().warning(Res.get("funds.tx.dustAttackTx.popup")).show()); + setGraphic(hyperlinkWithIcon); + } else { + setGraphic(new AutoTooltipLabel(item.getDetails())); + } } } else { setGraphic(null); - if (field != null) - field.setOnAction(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); } } }; diff --git a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java index 685f931d766..97cf1feb7ed 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java @@ -30,6 +30,8 @@ import bisq.desktop.util.Layout; import bisq.core.app.BisqEnvironment; +import bisq.core.btc.BaseCurrencyNetwork; +import bisq.core.btc.wallet.Restrictions; import bisq.core.dao.DaoFacade; import bisq.core.dao.governance.asset.AssetService; import bisq.core.filter.FilterManager; @@ -45,6 +47,7 @@ import bisq.core.user.BlockChainExplorer; import bisq.core.user.Preferences; import bisq.core.util.BSFormatter; +import bisq.core.util.validation.IntegerValidator; import bisq.common.UserThread; import bisq.common.app.DevEnv; @@ -87,6 +90,7 @@ import java.util.concurrent.TimeUnit; import static bisq.desktop.util.FormBuilder.*; +import static com.google.common.base.Preconditions.checkArgument; @FxmlView public class PreferencesView extends ActivatableViewAndModel { @@ -102,8 +106,9 @@ public class PreferencesView extends ActivatableViewAndModel allCryptoCurrencies; private ObservableList tradeCurrencies; private InputTextField deviationInputTextField; - private ChangeListener deviationListener, ignoreTradersListListener, referralIdListener, rpcUserListener, rpcPwListener; + private ChangeListener deviationListener, ignoreTradersListListener, ignoreDustThresholdListener, + /*referralIdListener,*/ rpcUserListener, rpcPwListener; private ChangeListener deviationFocusedListener; private ChangeListener useCustomFeeCheckboxListener; private ChangeListener transactionFeeChangeListener; @@ -324,6 +330,27 @@ public BaseCurrencyNetwork fromString(String string) { referralIdService.setReferralId(newValue); };*/ + + // ignoreDustThreshold + ignoreDustThresholdInputTextField = addInputTextField(root, ++gridRow, Res.get("setting.preferences.ignoreDustThreshold")); + IntegerValidator validator = new IntegerValidator(); + validator.setMinValue((int) Restrictions.getMinNonDustOutput().value); + validator.setMaxValue(2000); + ignoreDustThresholdInputTextField.setValidator(validator); + ignoreDustThresholdListener = (observable, oldValue, newValue) -> { + try { + int value = Integer.parseInt(newValue); + checkArgument(value >= Restrictions.getMinNonDustOutput().value, + "Input must be at least " + Restrictions.getMinNonDustOutput().value); + checkArgument(value <= 2000, + "Input must not be higher than 2000 Satoshis"); + if (!newValue.equals(oldValue)) { + preferences.setIgnoreDustThreshold(value); + } + } catch (Throwable ignore) { + } + }; + // AvoidStandbyModeService avoidStandbyMode = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.avoidStandbyMode")); @@ -608,6 +635,7 @@ private void activateGeneralOptions() { ignoreTradersListInputTextField.setText(String.join(", ", preferences.getIgnoreTradersList())); /* referralIdService.getOptionalReferralId().ifPresent(referralId -> referralIdInputTextField.setText(referralId)); referralIdInputTextField.setPromptText(Res.get("setting.preferences.refererId.prompt"));*/ + ignoreDustThresholdInputTextField.setText(String.valueOf(preferences.getIgnoreDustThreshold())); userLanguageComboBox.setItems(languageCodes); userLanguageComboBox.getSelectionModel().select(preferences.getUserLanguage()); userLanguageComboBox.setConverter(new StringConverter<>() { @@ -693,6 +721,7 @@ public BlockChainExplorer fromString(String string) { ignoreTradersListInputTextField.textProperty().addListener(ignoreTradersListListener); useCustomFee.selectedProperty().addListener(useCustomFeeCheckboxListener); //referralIdInputTextField.textProperty().addListener(referralIdListener); + ignoreDustThresholdInputTextField.textProperty().addListener(ignoreDustThresholdListener); } private Coin getTxFeeForWithdrawalPerByte() { @@ -857,6 +886,7 @@ private void deactivateGeneralOptions() { ignoreTradersListInputTextField.textProperty().removeListener(ignoreTradersListListener); useCustomFee.selectedProperty().removeListener(useCustomFeeCheckboxListener); //referralIdInputTextField.textProperty().removeListener(referralIdListener); + ignoreDustThresholdInputTextField.textProperty().removeListener(ignoreDustThresholdListener); } private void deactivateDisplayCurrencies() {