From a0dc4489f7c1b401989b9e518861e720de6c72de Mon Sep 17 00:00:00 2001 From: denavila Date: Fri, 20 Oct 2023 16:51:29 -0700 Subject: [PATCH] Deniability - a tool to automatically improve coin ownership privacy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This new feature is an implementation of the ideas in Paul Sztorc's blog post "Deniability - Unilateral Transaction Meta-Privacy"(https://www.truthcoin.info/blog/deniability/). In short, the idea is to periodically split coins and send them to yourself, making it look like a common "spend" transaction, such that blockchain ownership analysis becomes more difficult, and thus improving the user's privacy. This is the GUI portion of the PR (bitcoin-core/gui). The core functionality PR is in the main repo (bitcoin/bitcoin). This PR implements an additional "Deniability" wallet view. The majority of the GUI code is in a new deniabilitydialog.cpp/h source files containing a new DeniabilityDialog class, hooked up via the WalletView class.  On startup and on notable events (new blocks, new transactions, etc), we evaluate the privacy of all coins in the wallet, and we build a "deniabilization" candidate list. UTXOs that share the same destination address are grouped together into a single candidate (see DeniabilityDialog::updateCoins and DeniabilityDialog::updateCoinTable). We inspect the blockchain data to find out if we have performed "deniabilization" transactions already, and we count how many "cycles" (self-sends) have been performed for each coin (see DeniabilityDialog::calculateDeniabilizationStats). Since we infer the state entirely from the blockchain data, even if the wallet is restored from a seed phrase, the state would not be lost. This also means that if the user has performed manual self-sends that have improved the ownership privacy, they will be counted too. The user can initiate the "deniabillization" process by pressing a Start button (DeniabilityDialog::startDeniabilization). The process periodically perform a "deniabilization" cycle (DeniabilityDialog::deniabilizeProc). Each such cycle goes as follows: A coin is selected form the candidate list. The more a coin is "deniabilized", the less likely it is to be selected. Smaller coins are also less likely to be selected. If a coin is selected, we prepare and broadcast a transaction, which splits the coin into a pair of new wallet addresses (DeniabilityDialog::deniabilizeCoin).  The user can control this process via a Frequency selector and a Budget spinner, which respectively determine how often to perform the cycle and how much total BTC to spend on transaction fees. If Bitcoin Core is left running continuously, the cycles would be performed at the selected frequency (with some randomization). If Bitcoin Core is shutdown, the "deniabilization" process will resume at the next restart, and if more time has elapsed than the selected frequency, it will perform a single cycle. We deliberately don't "catch up" all missed cycles, since that would expose the process to blockchain analysis. The state is saved and restored via QSettings (DeniabilityDialog::loadSettings and DeniabilityDialog::saveSettings). We monitor each broadcasted transaction and we automatically attempt a fee bump if the transaction is still in the memory pool since the previous cycle (DeniabilityDialog::bumpDeniabilizationTx). We don't issue any other deniabilization transactions until the previous transaction is confirmed (or abandoned/dropped). The process ends when the budget is exhausted or if there's no candidates left. The user can also stop the process manually by pressing a Stop button (DeniabilityDialog::stopDeniabilization). External signers are supported in a "best effort" way - since the user needs to manually sign, we postpone the processing till the external signer is connected and use some additional UI to get the user's attention to sign (see the codepath predicated on hasExternalSigner). This is not ideal, so I'm looking for some ideas if we can improve this in some way. Watch-only wallets are partially supported, where we display the candidate list, but we don't allow any processing (since we don't have the private keys to issue transactions). I've tested all this functionality on regtest, testnet, signet and mainnet. I've also added some unit tests (under WalletTests) to exercise the main GUI functionality. This is my first change and PR for Bitcoin Core, and I tried as much as possible to validate everything against the guidelines and documentation and to follow the patterns in the existing code, but I'm sure there are things I missed, so I'm looking forward to your feedback. In particular things I'm not very sure about - the save/restore of state via QSettings makes me a bit nervous as we store some wallet specific data there which I put some effort to validate on load, however keying the settings based on wallet name is not ideal, so I'd like to improve this somehow - either by storing the settings based on some wallet identity signature, or by storing the state in the wallet database (however that doesn't seem accessible via the interfaces::Wallet API). Please let me know your thoughts and suggestions. ----- Refactored the coin update to explicitly match utxos by scriptPubKey and not rely on the ListCoin destination grouping. ----- Fixed linter error about bitcoin-config.h not being included. ---- Post-rebase fixes for cmake build. --- src/qt/CMakeLists.txt | 2 + src/qt/bitcoin.qrc | 1 + src/qt/bitcoingui.cpp | 18 + src/qt/bitcoingui.h | 3 + src/qt/deniabilitydialog.cpp | 1484 +++++++++++++++++++++++++++++ src/qt/deniabilitydialog.h | 190 ++++ src/qt/forms/deniabilitydialog.ui | 179 ++++ src/qt/res/icons/crosseye.png | Bin 0 -> 21985 bytes src/qt/test/wallettests.cpp | 222 ++++- src/qt/walletframe.cpp | 7 + src/qt/walletframe.h | 2 + src/qt/walletview.cpp | 16 + src/qt/walletview.h | 4 + 13 files changed, 2121 insertions(+), 7 deletions(-) create mode 100644 src/qt/deniabilitydialog.cpp create mode 100644 src/qt/deniabilitydialog.h create mode 100644 src/qt/forms/deniabilitydialog.ui create mode 100644 src/qt/res/icons/crosseye.png diff --git a/src/qt/CMakeLists.txt b/src/qt/CMakeLists.txt index dc62d0f57e2..72dd84fd38d 100644 --- a/src/qt/CMakeLists.txt +++ b/src/qt/CMakeLists.txt @@ -158,6 +158,8 @@ if(ENABLE_WALLET) coincontroltreewidget.h createwalletdialog.cpp createwalletdialog.h + deniabilitydialog.cpp + deniabilitydialog.h editaddressdialog.cpp editaddressdialog.h openuridialog.cpp diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc index fed373e551c..24584c50ceb 100644 --- a/src/qt/bitcoin.qrc +++ b/src/qt/bitcoin.qrc @@ -44,6 +44,7 @@ res/icons/hd_disabled.png res/icons/network_disabled.png res/icons/proxy.png + res/icons/crosseye.png res/animation/spinner-000.png diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 6d66c7473bd..c7e603ed818 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -277,6 +277,13 @@ void BitcoinGUI::createActions() historyAction->setShortcut(QKeySequence(QStringLiteral("Alt+4"))); tabGroup->addAction(historyAction); + deniabilityAction = new QAction(platformStyle->SingleColorIcon(":/icons/crosseye"), tr("&Deniability"), this); + deniabilityAction->setStatusTip(tr("Improve coin ownership privacy")); + deniabilityAction->setToolTip(deniabilityAction->statusTip()); + deniabilityAction->setCheckable(true); + deniabilityAction->setShortcut(QKeySequence(QStringLiteral("Alt+5"))); + tabGroup->addAction(deniabilityAction); + #ifdef ENABLE_WALLET // These showNormalIfMinimized are needed because Send Coins and Receive Coins // can be triggered from the tray menu, and need to show the GUI to be useful. @@ -288,6 +295,8 @@ void BitcoinGUI::createActions() connect(receiveCoinsAction, &QAction::triggered, this, &BitcoinGUI::gotoReceiveCoinsPage); connect(historyAction, &QAction::triggered, [this]{ showNormalIfMinimized(); }); connect(historyAction, &QAction::triggered, this, &BitcoinGUI::gotoHistoryPage); + connect(deniabilityAction, &QAction::triggered, [this] { showNormalIfMinimized(); }); + connect(deniabilityAction, &QAction::triggered, this, &BitcoinGUI::gotoDeniabilityPage); #endif // ENABLE_WALLET quitAction = new QAction(tr("E&xit"), this); @@ -597,6 +606,7 @@ void BitcoinGUI::createToolBars() toolbar->addAction(sendCoinsAction); toolbar->addAction(receiveCoinsAction); toolbar->addAction(historyAction); + toolbar->addAction(deniabilityAction); overviewAction->setChecked(true); #ifdef ENABLE_WALLET @@ -818,6 +828,7 @@ void BitcoinGUI::setWalletActionsEnabled(bool enabled) sendCoinsAction->setEnabled(enabled); receiveCoinsAction->setEnabled(enabled); historyAction->setEnabled(enabled); + deniabilityAction->setEnabled(enabled); encryptWalletAction->setEnabled(enabled); backupWalletAction->setEnabled(enabled); changePassphraseAction->setEnabled(enabled); @@ -979,6 +990,12 @@ void BitcoinGUI::gotoHistoryPage() if (walletFrame) walletFrame->gotoHistoryPage(); } +void BitcoinGUI::gotoDeniabilityPage() +{ + deniabilityAction->setChecked(true); + if (walletFrame) walletFrame->gotoDeniabilityPage(); +} + void BitcoinGUI::gotoReceiveCoinsPage() { receiveCoinsAction->setChecked(true); @@ -1294,6 +1311,7 @@ void BitcoinGUI::changeEvent(QEvent *e) sendCoinsAction->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/send"))); receiveCoinsAction->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/receiving_addresses"))); historyAction->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/history"))); + deniabilityAction->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/crosseye"))); } QMainWindow::changeEvent(e); diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index 73adbda5a5f..da7a65ced59 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -134,6 +134,7 @@ class BitcoinGUI : public QMainWindow QToolBar* appToolBar = nullptr; QAction* overviewAction = nullptr; QAction* historyAction = nullptr; + QAction* deniabilityAction = nullptr; QAction* quitAction = nullptr; QAction* sendCoinsAction = nullptr; QAction* usedSendingAddressesAction = nullptr; @@ -279,6 +280,8 @@ public Q_SLOTS: void gotoOverviewPage(); /** Switch to history (transactions) page */ void gotoHistoryPage(); + /** Switch to deniability (ownership obfuscation) page */ + void gotoDeniabilityPage(); /** Switch to receive coins page */ void gotoReceiveCoinsPage(); /** Switch to send coins page */ diff --git a/src/qt/deniabilitydialog.cpp b/src/qt/deniabilitydialog.cpp new file mode 100644 index 00000000000..b4aaf58099b --- /dev/null +++ b/src/qt/deniabilitydialog.cpp @@ -0,0 +1,1484 @@ +// Copyright (c) 2022- The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include // IWYU pragma: keep + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using common::PSBTError; +using interfaces::Wallet; + +enum { + COLUMN_CHECKBOX, + COLUMN_DESTINATION, + COLUMN_UTXO_COUNT, + COLUMN_AMOUNT, + COLUMN_DENIABILIZATION_CYCLES, + COLUMN_ESTIMATED_FEE, + COLUMN_COUNT +}; + +constexpr CAmount MAX_DENIABILIZATION_BUDGET = 100000; // 1 mBTC + +static bool hasExternalSigner(Wallet& wallet) +{ +#ifdef ENABLE_EXTERNAL_SIGNER + if (wallet.hasExternalSigner()) { + return true; + } +#endif + return false; +} + +static bool externalSignerConnected() +{ + std::vector signers; +#ifdef ENABLE_EXTERNAL_SIGNER + const std::string command = gArgs.GetArg("-signer", ""); + if (command.empty()) + return false; + try { + ExternalSigner::Enumerate(command, signers, Params().GetChainTypeString()); + } catch (const std::runtime_error& e) { + (void)e.what(); + } +#endif + return signers.size() == 1; +} + +CScript DeniabilityDialog::CoinInfo::scriptPubKey() const +{ + Assert(!utxos.empty()); + return utxos.front().walletTxOut.txout.scriptPubKey; +} + +CTxDestination DeniabilityDialog::CoinInfo::destination() const +{ + CTxDestination destination = CNoDestination(); + if (!ExtractDestination(scriptPubKey(), destination)) { + // For backwards compatibility, we convert P2PK output scripts into PKHash destinations + if (auto pubKeyDestination = std::get_if(&destination)) { + destination = PKHash(pubKeyDestination->GetPubKey()); + } + } + return destination; +} + +uint256 DeniabilityDialog::CoinInfo::hash() const +{ + HashWriter hasher{}; + for (const auto& utxo : utxos) { + hasher << utxo.outpoint.hash; + hasher << utxo.outpoint.n; + } + return hasher.GetSHA256(); +} + +CAmount DeniabilityDialog::CoinInfo::value() const +{ + CAmount amount = 0; + for (const auto& utxo : utxos) { + amount += utxo.walletTxOut.txout.nValue; + } + return amount; +} + +int DeniabilityDialog::CoinInfo::depthInMainChain() const +{ + int depth = INT_MAX; + for (const auto& utxo : utxos) { + depth = std::min(depth, utxo.walletTxOut.depth_in_main_chain); + } + return depth; +} + +uint DeniabilityDialog::CoinInfo::deniabilizationCycles() const +{ + uint cycles = UINT_MAX; + for (const auto& utxo : utxos) { + cycles = std::min(cycles, utxo.deniabilizationStats.cycles); + } + return cycles; +} + +bool DeniabilityDialog::CoinInfo::allUTXOsAreBlockReward() const +{ + for (const auto& utxo : utxos) { + if (!utxo.deniabilizationStats.blockReward) { + return false; + } + } + return true; +} + +bool DeniabilityDialog::CoinInfo::anyLockedCoin(interfaces::Wallet& wallet) const +{ + for (const auto& utxo : utxos) { + if (wallet.isLockedCoin(utxo.outpoint)) { + return true; + } + } + return false; +} + +DeniabilityDialog::DeniabilityDialog(const PlatformStyle* platformStyle, QWidget* parent) : QDialog(parent, GUIUtil::dialog_flags), + m_ui(new Ui::DeniabilityDialog), + m_platformStyle(platformStyle) +{ + m_ui->setupUi(this); + + setupTableWidget(); + + m_ui->budgetSpinner->setDisplayUnit(BitcoinUnit::SAT); + m_ui->budgetSpinner->SetMaxValue(MAX_DENIABILIZATION_BUDGET); + m_ui->budgetSpinner->setSingleStep(1000); + + m_deniabilizeProcTimer = new QTimer(this); + connect(m_deniabilizeProcTimer, SIGNAL(timeout()), this, SLOT(deniabilizeProc())); + + m_contextMenu = new QMenu(this); + m_contextMenu->setObjectName("contextMenuDeniability"); + + QAction* copyAddress = new QAction(tr("Copy Address"), this); + m_contextMenu->addAction(copyAddress); + + connect(copyAddress, &QAction::triggered, this, [this]() { + auto selectionModel = m_ui->tableWidgetCoins->selectionModel(); + if (!selectionModel) + return; + QModelIndexList selection = selectionModel->selectedRows(); + if (!selection.isEmpty()) { + GUIUtil::setClipboard(selection.at(0).data(destinationRole).toString()); + } + }); + + QAction* copyTxHash = new QAction(tr("Copy Transaction ID"), this); + m_contextMenu->addAction(copyTxHash); + + connect(copyTxHash, &QAction::triggered, this, [this]() { + auto selectionModel = m_ui->tableWidgetCoins->selectionModel(); + if (!selectionModel) + return; + QModelIndexList selection = selectionModel->selectedRows(); + if (!selection.isEmpty()) { + std::string destinationStr = selection.at(0).data(destinationRole).toString().toStdString(); + CTxDestination destination = DecodeDestination(destinationStr); + for (auto& coin : m_coinsList) { + if (coin.destination() == destination) { + QString hashStr = QString::fromStdString(coin.utxos.front().outpoint.hash.GetHex()); + GUIUtil::setClipboard(hashStr); + break; + } + } + } + }); +} + +DeniabilityDialog::~DeniabilityDialog() +{ + m_deniabilizeProcTimer->stop(); + delete m_deniabilizeProcTimer; + m_deniabilizeProcTimer = nullptr; + + saveSettings(); + + delete m_ui; +} + +void DeniabilityDialog::setupTableWidget() +{ + static_assert(COLUMN_COUNT == 6, "Update the header names below for any change in columns"); + QStringList headerLables; + headerLables << ""; + headerLables << tr("Address"); + headerLables << tr("UTXO Count"); + headerLables << tr("Amount") + " (" + BitcoinUnits::shortName(m_displayUnit) + ")"; + headerLables << tr("Cycles"); + headerLables << tr("Estimated Fees") + " (" + BitcoinUnits::shortName(BitcoinUnits::Unit::SAT) + ")"; + + // Setup coin table + m_ui->tableWidgetCoins->setColumnCount(COLUMN_COUNT); + m_ui->tableWidgetCoins->setHorizontalHeaderLabels(headerLables); + m_ui->tableWidgetCoins->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft); + + // Resize cells (in a backwards compatible way) +#if QT_VERSION < 0x050000 + m_ui->tableWidgetCoins->horizontalHeader()->setResizeMode(QHeaderView::ResizeToContents); +#else + m_ui->tableWidgetCoins->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); +#endif + m_ui->tableWidgetCoins->horizontalHeader()->setStretchLastSection(false); + m_ui->tableWidgetCoins->verticalHeader()->setVisible(false); + + // Select rows + m_ui->tableWidgetCoins->setSelectionBehavior(QAbstractItemView::SelectRows); + + // connect selection/deselection + connect(m_ui->tableWidgetCoins, &QTableWidget::itemClicked, this, &DeniabilityDialog::updateCheckState); + + // Apply custom context menu + m_ui->tableWidgetCoins->setContextMenuPolicy(Qt::CustomContextMenu); + + // Connect context menus + connect(m_ui->tableWidgetCoins, &QWidget::customContextMenuRequested, this, &DeniabilityDialog::contextualMenu); +} + +void DeniabilityDialog::contextualMenu(const QPoint& point) +{ + QModelIndex index = m_ui->tableWidgetCoins->indexAt(point); + if (index.isValid() && index.column() == COLUMN_DESTINATION) { + m_contextMenu->popup(m_ui->tableWidgetCoins->viewport()->mapToGlobal(point)); + } +} + +void DeniabilityDialog::loadSettings() +{ + // we don't store settings if there's no wallet name to avoid contaminating settings between different unnamed wallets + if (m_walletName.empty()) { + m_deniabilizationBudget = 0; + m_ui->budgetSpinner->setValue(m_deniabilizationBudget); + m_deniabilizationFrequency = std::chrono::hours(24); + m_ui->dailyRadioButton->setChecked(true); + m_nextDeniabilizationCycle.reset(); + m_lastDeniabilizationTxHash.reset(); + m_coinStateMap.clear(); + return; + } + + Assert(m_model); + Wallet& wallet = m_model->wallet(); + + QSettings settings; + settings.beginGroup("Deniability[" + QString::fromStdString(m_walletName) + "]"); + + if (!settings.contains("nDeniabilizationBudget")) { + settings.setValue("nDeniabilizationBudget", (qint64)0); + } + if (!settings.contains("nDeniabilizationFrequency")) { + settings.setValue("nDeniabilizationFrequency", (quint64)(60 * 60 * 24)); // 60 seconds to a minute, 60 minutes to an hour, 24 hours to a day + } + if (!settings.contains("nNextDeniabilizationCycle")) { + settings.setValue("nNextDeniabilizationCycle", (quint64)0); + } + if (!settings.contains("fDeniabilizationProcessAccepted")) { + settings.setValue("fDeniabilizationProcessAccepted", false); + } + if (!settings.contains("sLastDeniabilizationTxHash")) { + settings.setValue("sLastDeniabilizationTxHash", ""); + } + + CAmount nDeniabilizationBudget = settings.value("nDeniabilizationBudget").toLongLong(); + if (nDeniabilizationBudget < 0) { + nDeniabilizationBudget = 0; + } else if (nDeniabilizationBudget > MAX_DENIABILIZATION_BUDGET) { + nDeniabilizationBudget = MAX_DENIABILIZATION_BUDGET; + } + m_deniabilizationBudget = nDeniabilizationBudget; + m_ui->budgetSpinner->setValue(m_deniabilizationBudget); + + uint64_t nDeniabilizationFrequency = settings.value("nDeniabilizationFrequency").toULongLong(); + m_deniabilizationFrequency = std::chrono::seconds(nDeniabilizationFrequency); + + if (m_deniabilizationFrequency == std::chrono::hours(1)) { + m_ui->hourlyRadioButton->setChecked(true); + } else if (m_deniabilizationFrequency == std::chrono::hours(24)) { + m_ui->dailyRadioButton->setChecked(true); + } else if (m_deniabilizationFrequency == std::chrono::hours(24 * 7)) { + m_ui->weeklyRadioButton->setChecked(true); + } else { + m_deniabilizationFrequency = std::chrono::hours(24); + m_ui->dailyRadioButton->setChecked(true); + } + + m_nextDeniabilizationCycle.reset(); + uint64_t nNextDeniabilizationCycle = settings.value("nNextDeniabilizationCycle").toULongLong(); + if (nNextDeniabilizationCycle) { + m_nextDeniabilizationCycle = std::chrono::system_clock::time_point(std::chrono::system_clock::duration(nNextDeniabilizationCycle)); + } + + m_deniabilizationProcessAccepted = settings.value("fDeniabilizationProcessAccepted").toBool(); + + m_lastDeniabilizationTxHash.reset(); + QString hashStr = settings.value("sLastDeniabilizationTxHash").toString(); + if (!hashStr.isEmpty()) { + uint256 hash = uint256S(hashStr.toStdString()); + if (wallet.getTx(hash)) { + m_lastDeniabilizationTxHash = hash; + } + } + + m_coinStateMap.clear(); + int coinCount = settings.beginReadArray("coinCheckStateArray"); + for (int coinIndex = 0; coinIndex < coinCount; coinIndex++) { + settings.setArrayIndex(coinIndex); + QString hashStr = settings.value("hash").toString(); + uint256 hash = uint256S(hashStr.toStdString()); + CoinState coinState; + coinState.deniabilizable = (Deniabilizable)settings.value("deniabilizable").toUInt(); + bool validStatus = false; + switch (coinState.deniabilizable) { + case Deniabilizable::YES: + case Deniabilizable::YES_BUT_BLOCK_REWARD: + case Deniabilizable::YES_BUT_COIN_LOCKED: + case Deniabilizable::YES_BUT_TX_NOT_MATURE: + case Deniabilizable::YES_BUT_AMOUNT_NOT_WORTHWHILE: + case Deniabilizable::NO_FULLY_DENIABILIZED: + case Deniabilizable::NO_PRIVATE_KEYS_DISABLED: + case Deniabilizable::NO_AMOUNT_TOO_SMALL: + case Deniabilizable::NO: + validStatus = true; + break; + } + if (!validStatus) + continue; + coinState.checkState = (Qt::CheckState)settings.value("checkState").toUInt(); + if (!(coinState.checkState == Qt::Checked || coinState.checkState == Qt::Unchecked)) + continue; + m_coinStateMap[hash] = coinState; + } + settings.endArray(); + + settings.endGroup(); +} + +void DeniabilityDialog::saveSettings() +{ + if (m_walletName.empty()) { + return; + } + + QSettings settings; + settings.beginGroup("Deniability[" + QString::fromStdString(m_walletName) + "]"); + + settings.setValue("nDeniabilizationBudget", (qint64)m_deniabilizationBudget); + + uint64_t nDeniabilizationFrequency = m_deniabilizationFrequency.count(); + settings.setValue("nDeniabilizationFrequency", (quint64)nDeniabilizationFrequency); + + uint64_t nNextDeniabilizationCycle = 0; + if (m_nextDeniabilizationCycle.has_value()) { + nNextDeniabilizationCycle = m_nextDeniabilizationCycle.value().time_since_epoch().count(); + } + settings.setValue("nNextDeniabilizationCycle", (quint64)nNextDeniabilizationCycle); + + settings.setValue("fDeniabilizationProcessAccepted", m_deniabilizationProcessAccepted); + + if (m_lastDeniabilizationTxHash) { + uint256 hash = m_lastDeniabilizationTxHash.value(); + QString hashStr = QString::fromStdString(hash.ToString()); + settings.setValue("sLastDeniabilizationTxHash", hashStr); + } else { + settings.setValue("sLastDeniabilizationTxHash", ""); + } + + settings.beginWriteArray("coinCheckStateArray"); + int coinIndex = 0; + for (const auto& coin : m_coinsList) { + // only store the the state of coins that may be deniabilized + if (coin.state.mayBeDeniabilized()) { + settings.setArrayIndex(coinIndex++); + uint256 hash = coin.hash(); + QString hashStr = QString::fromStdString(hash.GetHex()); + settings.setValue("hash", hashStr); + settings.setValue("deniabilizable", (uint)coin.state.deniabilizable); + settings.setValue("checkState", (uint)coin.state.checkState); + } + } + settings.endArray(); + + settings.endGroup(); +} + +void DeniabilityDialog::updateCheckState(QTableWidgetItem* itemCheck) +{ + std::string destinationStr = itemCheck->data(destinationRole).toString().toStdString(); + CTxDestination destination = DecodeDestination(destinationStr); + Qt::CheckState checkState = itemCheck->checkState(); + + for (auto& coin : m_coinsList) { + if (coin.destination() == destination) { + coin.state.checkState = checkState; + break; + } + } + + updateStart(); + updateStatus(); +} + +bool DeniabilityDialog::walletSupportsDeniabilization() const +{ + if (!m_model) + return false; + + Wallet& wallet = m_model->wallet(); + if (wallet.privateKeysDisabled() && !hasExternalSigner(wallet)) { + return false; + } + + if (wallet.isLegacy()) { + return false; + } + + return true; +} + + +void DeniabilityDialog::updateStart() +{ + if (m_ui->stopButton->isEnabled()) { + // stop button is active that means start button should not be + Assert(!m_ui->startButton->isEnabled()); + return; + } + + CAmount budgetValue = m_ui->budgetSpinner->value(); + bool hasCandidates = hasDeniabilizationCandidates(); + + // disable or enable the start button depending on the budget provided and the availability of candidates + if (m_ui->startButton->isEnabled()) { + if (budgetValue == 0 || !hasCandidates) { + m_ui->startButton->setEnabled(false); + } + } else { + if (budgetValue > 0 && hasCandidates) { + m_ui->startButton->setEnabled(true); + } + } +} + +void DeniabilityDialog::updateStatus() +{ + if (!m_model) { + m_ui->statusLabel->setText(tr("Deniabilization is not supported without a wallet")); + return; + } + + if (!m_clientModel || m_clientModel->node().isInitialBlockDownload()) { + m_ui->statusLabel->setText(tr("Waiting for blockchain data to synchronize...")); + return; + } + + if (!walletSupportsDeniabilization()) { + Wallet& wallet = m_model->wallet(); + if (wallet.privateKeysDisabled() && !hasExternalSigner(wallet)) { + m_ui->statusLabel->setText(tr("Deniabilization is not supported without private keys")); + return; + } + + if (wallet.isLegacy()) { + m_ui->statusLabel->setText(tr("Deniabilization is not supported on legacy wallets")); + return; + } + } + + if (m_ui->startButton->isEnabled()) { + Assert(!m_ui->stopButton->isEnabled()); + // start button is enabled which means a non-zero budget was entered + if (hasDeniabilizationCandidates()) { + m_ui->statusLabel->setText(tr("Deniabilization process is not active. Choose a frequency and press Start to begin.")); + } else { + m_ui->statusLabel->setText(tr("No deniabilization candidates available.")); + } + return; + } + + if (m_ui->stopButton->isEnabled()) { + if (m_deniabilizationTxInProgress) { + m_ui->statusLabel->setText(tr("Deniabilization cycle in progress...")); + return; + } + + if (m_lastDeniabilizationTxHash) { + m_ui->statusLabel->setText(tr("Waiting for the deniabilization transaction to be confirmed...")); + return; + } + + Assert(m_nextDeniabilizationCycle.has_value()); + auto timeNow = std::chrono::system_clock::now(); + if (timeNow < m_nextDeniabilizationCycle.value()) { + auto deltaMinutes = std::chrono::duration_cast(m_nextDeniabilizationCycle.value() - timeNow); + QString deltaTimeStr; + if (deltaMinutes < std::chrono::minutes(60)) { + deltaTimeStr = QString::number(deltaMinutes.count()) + " " + tr("minutes"); + } else { + auto deltaHours = std::chrono::duration_cast(deltaMinutes); + deltaMinutes -= std::chrono::minutes(deltaHours); + if (deltaHours < std::chrono::hours(24)) { + deltaTimeStr = QString::number(deltaHours.count()) + " " + tr("hours and") + " " + QString::number(deltaMinutes.count()) + " " + tr("minutes"); + } else { + auto deltaDays = deltaHours / 24; + deltaHours -= deltaDays * 24; + deltaTimeStr = QString::number(deltaDays.count()) + " " + tr("days") + ", " + QString::number(deltaHours.count()) + " " + tr("hours and") + " " + QString::number(deltaMinutes.count()) + " " + tr("minutes"); + } + } + m_ui->statusLabel->setText(tr("Next deniabilization cycle in") + " " + deltaTimeStr + ". " + tr("Press Stop to cancel.")); + } else { + Wallet& wallet = m_model->wallet(); + if (hasExternalSigner(wallet)) { + m_ui->statusLabel->setText(tr("Deniabilization process is active. Waiting on external signer to be connected...")); + } else { + m_ui->statusLabel->setText(tr("Deniabilization cycle is about to begin...")); + } + } + return; + } + + // both start and stop buttons are not active, so we're waiting for a budget to be entered + if (hasDeniabilizationCandidates()) { + m_ui->statusLabel->setText(tr("Deniabilization process is not active. Choose a frequency and a budget, and then press Start.")); + } else { + m_ui->statusLabel->setText(tr("No deniabilization candidates available.")); + } +} + +void DeniabilityDialog::updateCoinTable() +{ + m_ui->tableWidgetCoins->setUpdatesEnabled(false); + + m_ui->tableWidgetCoins->setRowCount(0); + + int nRow = 0; + for (const auto& coin : m_coinsList) { + m_ui->tableWidgetCoins->insertRow(nRow); + + QString destinationStr = QString::fromStdString(EncodeDestination(coin.destination())); + + static_assert(COLUMN_COUNT == 6, "Update the item logic below for any change in columns"); + + { + // Checkbox + QTableWidgetItem* itemCheck = new QTableWidgetItem(); + itemCheck->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); + itemCheck->setCheckState(Qt::Unchecked); + itemCheck->setData(destinationRole, destinationStr); + if (coin.state.mayBeDeniabilized()) { + itemCheck->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable); + } else { + itemCheck->setFlags(Qt::NoItemFlags); + } + itemCheck->setCheckState(coin.state.checkState); + m_ui->tableWidgetCoins->setItem(nRow, COLUMN_CHECKBOX, itemCheck); + } + + { + // Destination + QTableWidgetItem* itemDestination = new QTableWidgetItem(); + itemDestination->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + itemDestination->setText(destinationStr); + // Keep the destination field always enabled so we can copy/paste the address + itemDestination->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + if (!coin.state.mayBeDeniabilized()) { + itemDestination->setForeground(Qt::gray); + } + switch (coin.state.deniabilizable) { + case Deniabilizable::YES: + itemDestination->setToolTip(tr("This coin can be deniabilized")); + break; + case Deniabilizable::YES_BUT_BLOCK_REWARD: + itemDestination->setToolTip(tr("This coin can be deniabilized but it's from a block reward and likely not needed")); + break; + case Deniabilizable::YES_BUT_COIN_LOCKED: + itemDestination->setToolTip(tr("This coin can be deniabilized but contains locked UTXOs, selecting it will unlock the coins during deniabilization")); + break; + case Deniabilizable::YES_BUT_TX_NOT_MATURE: + itemDestination->setToolTip(tr("This coin can be deniabilized but waiting for more confirmations is recommended")); + break; + case Deniabilizable::YES_BUT_AMOUNT_NOT_WORTHWHILE: + itemDestination->setToolTip(tr("This coin can be deniabilized but the amount is not worthwhile")); + break; + case Deniabilizable::NO_FULLY_DENIABILIZED: + itemDestination->setToolTip(tr("This coin is already fully deniabilized")); + break; + case Deniabilizable::NO_PRIVATE_KEYS_DISABLED: + itemDestination->setToolTip(tr("This coin can't be deniabilized because the wallet's private keys are disabled")); + break; + case Deniabilizable::NO_AMOUNT_TOO_SMALL: + itemDestination->setToolTip(tr("This coin can't be deniabilized because the coin amount is less than the estimated fees")); + break; + case Deniabilizable::NO: + itemDestination->setToolTip(tr("This coin can't be deniabilized")); + break; + } + m_ui->tableWidgetCoins->setItem(nRow, COLUMN_DESTINATION, itemDestination); + } + + { + // UTXO Count + QTableWidgetItem* itemUTXOCount = new QTableWidgetItem(); + itemUTXOCount->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); + itemUTXOCount->setText(QString::number(coin.numUTXOs())); + if (coin.state.mayBeDeniabilized()) { + itemUTXOCount->setFlags(Qt::ItemIsEnabled); + } else { + itemUTXOCount->setFlags(Qt::NoItemFlags); + itemUTXOCount->setForeground(Qt::gray); + } + m_ui->tableWidgetCoins->setItem(nRow, COLUMN_UTXO_COUNT, itemUTXOCount); + } + + { + // Amount + QTableWidgetItem* itemAmount = new QTableWidgetItem(); + itemAmount->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); + itemAmount->setText(BitcoinUnits::format(m_displayUnit, coin.value())); + if (coin.state.mayBeDeniabilized()) { + itemAmount->setFlags(Qt::ItemIsEnabled); + } else { + itemAmount->setFlags(Qt::NoItemFlags); + itemAmount->setForeground(Qt::gray); + } + m_ui->tableWidgetCoins->setItem(nRow, COLUMN_AMOUNT, itemAmount); + } + + { + // Deniabilization status + QTableWidgetItem* itemDeniabilization = new QTableWidgetItem(); + itemDeniabilization->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); + itemDeniabilization->setText(QString::number(coin.deniabilizationCycles())); + if (coin.state.mayBeDeniabilized()) { + itemDeniabilization->setFlags(Qt::ItemIsEnabled); + } else { + itemDeniabilization->setFlags(Qt::NoItemFlags); + itemDeniabilization->setForeground(Qt::gray); + } + // Set deniabilization cell highlight color + if (coin.allUTXOsAreBlockReward()) { + itemDeniabilization->setBackground(QColor(0, 128, 0, 128)); + } else { + float deniabilizationProbability = wallet::CalculateDeniabilizationProbability(coin.deniabilizationCycles()); + itemDeniabilization->setBackground(QColor(deniabilizationProbability * 128, (1.0f - deniabilizationProbability) * 128, 0, 128)); + } + m_ui->tableWidgetCoins->setItem(nRow, COLUMN_DENIABILIZATION_CYCLES, itemDeniabilization); + } + + { + // Estimated fee + QTableWidgetItem* itemEstimatedFee = new QTableWidgetItem(); + itemEstimatedFee->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); + itemEstimatedFee->setText(QString::number(coin.deniabilizationFeeEstimate)); + if (coin.state.mayBeDeniabilized()) { + itemEstimatedFee->setFlags(Qt::ItemIsEnabled); + } else { + itemEstimatedFee->setFlags(Qt::NoItemFlags); + itemEstimatedFee->setForeground(Qt::gray); + } + m_ui->tableWidgetCoins->setItem(nRow, COLUMN_ESTIMATED_FEE, itemEstimatedFee); + } + + nRow++; + } + + m_ui->tableWidgetCoins->setUpdatesEnabled(true); +} + +void DeniabilityDialog::startDeniabilization() +{ + Assert(m_model); + + if (!m_deniabilizationProcessAccepted) { + Wallet& wallet = m_model->wallet(); + + QString message = tr("Deniabilization is about to start:"); + message += "

"; + message += tr("A coin will be selected from the top of the candidate list."); + message += "
"; + message += tr("A transaction will be prepared to split the coin into a pair of new addresses in your wallet."); + message += "
"; + message += tr("This makes blockchain analysis harder and thus improves privacy with each \"deniabilization\" cycle."); + message += "
"; + if (hasExternalSigner(wallet)) { + message += tr("You'll be prompted to confirm the transaction on your hardware device, and it will be broadcast immediately."); + } else { + message += tr("The transaction will be broadcast immediately."); + } + message += "

"; + message += tr("If %1 is left running continuously, the above process will repeat at the selected frequency (with some amount of randomization).").arg(PACKAGE_NAME); + message += "

"; + message += tr("If %1 is shutdown and later restarted, the process will resume, and if the elapsed time has exceeded the selected frequency, it will prepare and broadcast a transaction immediately.").arg(PACKAGE_NAME); + message += "

"; + message += tr("You can exclude a particular candidate from being selected by unchecking the checkbox on the left."); + message += "

"; + message += tr("The deniabilization process will stop when the specified budget is exhausted or if it runs out of suitable candidates."); + message += "
"; + message += tr("You can also stop it at any time by pressing the Stop button."); + message += "

"; + message += tr("If you'd like to change the budget or frequency, press Cancel now."); + message += "
"; + message += tr("Otherwise, press Ok to continue."); + QMessageBox::StandardButton resultButton = QMessageBox::information(nullptr, tr("Starting deniabilization"), message, QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel); + if (resultButton == QMessageBox::Cancel) { + return; + } + m_deniabilizationProcessAccepted = true; + } + + // disable the start button + m_ui->startButton->setEnabled(false); + // disable all settings UI + m_ui->hourlyRadioButton->setEnabled(false); + m_ui->dailyRadioButton->setEnabled(false); + m_ui->weeklyRadioButton->setEnabled(false); + m_ui->budgetSpinner->setEnabled(false); + // enable the stop button + m_ui->stopButton->setEnabled(true); + + // if this is the first time we're running, schedule a deniabilization right away + if (!m_nextDeniabilizationCycle.has_value()) { + m_nextDeniabilizationCycle = std::chrono::system_clock::now(); + } + updateStatus(); + // update status every 10 seconds + m_deniabilizeProcTimer->start(std::chrono::seconds(10)); + deniabilizeProc(); +} + +void DeniabilityDialog::stopDeniabilization() +{ + Assert(m_model); + + m_deniabilizeProcTimer->stop(); + m_nextDeniabilizationCycle.reset(); + + // disable the stop button + m_ui->stopButton->setEnabled(false); + // enable back all settings UI + m_ui->hourlyRadioButton->setEnabled(true); + m_ui->dailyRadioButton->setEnabled(true); + m_ui->weeklyRadioButton->setEnabled(true); + m_ui->budgetSpinner->setEnabled(true); + + updateStart(); + updateStatus(); +} + +bool DeniabilityDialog::hasDeniabilizationCandidates() const +{ + // if the last tx hasn't confirmed yet, + // consider it a candidate to prevent the deniabilization process from stopping prematurely + if (m_lastDeniabilizationTxHash) + return true; + for (const auto& coin : m_coinsList) { + // if a coin is not selected but may become deniabilizable, + // consider it a candidate to prevent the deniabilization process from stopping prematurely + if (coin.state.checkState == Qt::Checked || coin.state.mayBeDeniabilized()) { + return true; + } + } + return false; +} + +enum TxStatus { + TX_UNKNOWN, + TX_CONFLICTING, + TX_ABANDONED, + TX_IN_MEMPOOL, + TX_CONFIRMED +}; + +static TxStatus getTxStatus(interfaces::Wallet& wallet, uint256 hash) +{ + int numBlocks; + interfaces::WalletTxStatus status; + interfaces::WalletOrderForm orderForm; + bool inMempool; + interfaces::WalletTx wtx = wallet.getWalletTxDetails(hash, status, orderForm, inMempool, numBlocks); + if (!wtx.tx) { + // transaction not found + return TX_UNKNOWN; + } else if (status.depth_in_main_chain < 0) { + // conflicting transaction + return TX_CONFLICTING; + } else if (status.depth_in_main_chain == 0) { + if (status.is_abandoned || !inMempool) { + // abandoned or dropped from the mempool + return TX_ABANDONED; + } else { + // still in the mempool + return TX_IN_MEMPOOL; + } + } else { + Assert(status.depth_in_main_chain > 0); + // the tx was included in a block + return TX_CONFIRMED; + } +} + +void DeniabilityDialog::deniabilizeProc() +{ + if (!m_model) + return; + + if (!m_clientModel || m_clientModel->node().isInitialBlockDownload()) { + updateStatus(); + return; + } + + Assert(m_nextDeniabilizationCycle.has_value()); + auto timeNow = std::chrono::system_clock::now(); + if (timeNow < m_nextDeniabilizationCycle.value()) { + updateStatus(); + return; + } + + if (m_deniabilizationTxInProgress) { + updateStatus(); + return; + } + + Wallet& wallet = m_model->wallet(); + if (hasExternalSigner(wallet)) { + if (!externalSignerConnected()) { + updateStatus(); + return; + } + } + + m_deniabilizationTxInProgress = true; + + updateCoins(); + + // check up on the last TX and make sure it's not still in the mempool + if (m_lastDeniabilizationTxHash) { + TxStatus txStatus = getTxStatus(wallet, m_lastDeniabilizationTxHash.value()); + if (txStatus == TX_IN_MEMPOOL) { + // if it's still in the mempool, try a fee bump + QString hashStr = QString::fromStdString(m_lastDeniabilizationTxHash.value().ToString()); + if (bumpDeniabilizationTx(m_lastDeniabilizationTxHash.value())) { + Assert(m_lastDeniabilizationTxHash.has_value()); + LogPrintf("Deniability[%s]: Fee bump transaction (%s) broadcasted successfully.\n", m_walletName, m_lastDeniabilizationTxHash.value().GetHex()); + // Update the table + m_model->getTransactionTableModel()->updateTransaction(hashStr, CT_UPDATED, true); + } else { + LogPrintf("Deniability[%s]: Skipping fee bump transaction.\n", m_walletName); + } + } + } + + // Check which coin can be deniabilized + bool stop = false; + if (!m_lastDeniabilizationTxHash) { + for (auto& coin : m_coinsList) { + if (coin.state.checkState == Qt::Unchecked) { + continue; + } + + DeniabilizationResult result = deniabilizeCoin(coin); + if (result == DENIABILIZATION_SKIP_COIN) { + // try the next coin now, but retry this coin at the next cycle + LogPrintf("Deniability[%s]: Skipping coin.\n", m_walletName); + continue; + } else if (result == DENIABILIZATION_SKIP_ALL_COINS) { + // don't try any other coins now, but retry the selected coins at the next cycle + LogPrintf("Deniability[%s]: Skipping all coins.\n", m_walletName); + break; + } else if (result == DENIABILIZATION_DESELECT_COIN) { + // deselect this coin so it won't retry at the next cycle (the user can still re-select manually) + coin.state.checkState = Qt::Unchecked; + LogPrintf("Deniability[%s]: Deselecting coin.\n", m_walletName); + // try the next coin now + continue; + } else if (result == DENIABILIZATION_STOP) { + // don't try any more coins and stop the processing, eg due to out of budget or other fatal error + stop = true; + LogPrintf("Deniability[%s]: Stopping the deniabilization process.\n", m_walletName); + break; + } else { + Assert(result == DENIABILIZATION_SUCCESS); + Assert(m_lastDeniabilizationTxHash.has_value()); + LogPrintf("Deniability[%s]: Transaction (%s) broadcasted successfully.\n", m_walletName, m_lastDeniabilizationTxHash.value().GetHex()); + updateCoins(); + break; + } + } + } + + if (!stop && m_deniabilizationBudget > 0 && hasDeniabilizationCandidates()) { + // Update the next deniabilization cycle time with the desired frequency with some randomization + uint64_t frequency = m_deniabilizationFrequency.count(); + uint64_t randomizedFrequency = frequency + FastRandomContext().randrange(frequency); + + timeNow = std::chrono::system_clock::now(); + m_nextDeniabilizationCycle = timeNow + std::chrono::seconds(randomizedFrequency); + } else { + // if stop-processing was requested or there's no coins left to deniabilize, turn off the deniabilization process + stopDeniabilization(); + } + + m_deniabilizationTxInProgress = false; + updateStatus(); +} + +void DeniabilityDialog::clear() +{ + // if deniabilization is running don't do anything + if (m_ui->stopButton->isEnabled()) { + return; + } + + // reset the UI to default values + m_ui->budgetSpinner->setValue(0); + m_ui->dailyRadioButton->setChecked(true); + updateStart(); + updateStatus(); +} + +void DeniabilityDialog::reject() +{ + clear(); +} + +void DeniabilityDialog::accept() +{ + clear(); +} + +void DeniabilityDialog::updateCoinsIfVisible() +{ + if (this->isVisible()) { + updateCoins(); + } +} + +void DeniabilityDialog::updateNumberOfBlocks(int count, const QDateTime& blockDate, double nVerificationProgress, SyncType synctype, SynchronizationState sync_state) +{ + if (sync_state == SynchronizationState::POST_INIT) { + updateCoinsIfVisible(); + } +} + +void DeniabilityDialog::setClientModel(ClientModel* clientModel) +{ + m_clientModel = clientModel; + + if (m_clientModel) { + connect(m_clientModel, &ClientModel::numBlocksChanged, this, &DeniabilityDialog::updateNumberOfBlocks); + } +} + +void DeniabilityDialog::setModel(WalletModel* model) +{ + m_model = model; + + if (walletSupportsDeniabilization()) { + Assert(m_model); + OptionsModel* optionsModel = m_model->getOptionsModel(); + if (optionsModel) { + connect(optionsModel, &OptionsModel::displayUnitChanged, this, [this, optionsModel]() { + m_displayUnit = optionsModel->getDisplayUnit(); + setupTableWidget(); + updateCoinsIfVisible(); + }); + } + + connect(m_model, &WalletModel::balanceChanged, this, &DeniabilityDialog::updateCoinsIfVisible); + + connect(m_ui->startButton, &QPushButton::clicked, this, &DeniabilityDialog::startDeniabilization); + connect(m_ui->stopButton, &QPushButton::clicked, this, &DeniabilityDialog::stopDeniabilization); + + connect(m_ui->budgetSpinner, &BitcoinAmountField::valueChanged, this, [this]() { + m_deniabilizationBudget = m_ui->budgetSpinner->value(); + updateStart(); + updateStatus(); + }); + + connect(m_ui->hourlyRadioButton, &QRadioButton::toggled, this, [this](bool checked) { + if (checked) { + m_deniabilizationFrequency = std::chrono::hours(1); + updateCoinsIfVisible(); + } + }); + connect(m_ui->dailyRadioButton, &QRadioButton::toggled, this, [this](bool checked) { + if (checked) { + m_deniabilizationFrequency = std::chrono::hours(24); + updateCoinsIfVisible(); + } + }); + connect(m_ui->weeklyRadioButton, &QRadioButton::toggled, this, [this](bool checked) { + if (checked) { + m_deniabilizationFrequency = std::chrono::hours(24 * 7); + updateCoinsIfVisible(); + } + }); + } else { + // disable all settings UI + m_ui->startButton->setEnabled(false); + m_ui->hourlyRadioButton->setEnabled(false); + m_ui->dailyRadioButton->setEnabled(false); + m_ui->weeklyRadioButton->setEnabled(false); + m_ui->budgetSpinner->setEnabled(false); + m_ui->stopButton->setEnabled(false); + } + + if (m_model) { + m_walletName = m_model->wallet().getWalletName(); + loadSettings(); + } else { + m_walletName.clear(); + m_deniabilizationBudget = 0; + m_deniabilizationFrequency = std::chrono::seconds::zero(); + m_nextDeniabilizationCycle.reset(); + } + + updateCoins(); + + // if the start button is enabled and we have a valid deniabilization time + // that means deniabilization was active at shutdown, so restart it right away + if (m_nextDeniabilizationCycle.has_value()) { + if (m_ui->startButton->isEnabled()) { + startDeniabilization(); + } else { + m_nextDeniabilizationCycle.reset(); + } + } +} + +void DeniabilityDialog::updateCoins() +{ + if (!m_model) { + m_coinsList.clear(); + updateCoinTable(); + updateStart(); + updateStatus(); + return; + } + + // wait for at least 6 confirmations before deniabilizing a coin + const int minChainDepth = 6; + + Wallet& wallet = m_model->wallet(); + + // check up on the last TX and clear it if was confirmed, abandoned or dropped from the pool + if (m_lastDeniabilizationTxHash) { + TxStatus txStatus = getTxStatus(wallet, m_lastDeniabilizationTxHash.value()); + switch (txStatus) { + case TX_IN_MEMPOOL: + // still in mempool + break; + case TX_CONFIRMED: + LogPrintf("Deniability[%s]: Deniabilization transaction (%s) was confirmed.\n", m_walletName, m_lastDeniabilizationTxHash.value().GetHex()); + m_lastDeniabilizationTxHash.reset(); + break; + case TX_CONFLICTING: + case TX_ABANDONED: + LogPrintf("Deniability[%s]: Deniabilization transaction (%s) was abandoned or dropped.\n", m_walletName, m_lastDeniabilizationTxHash.value().GetHex()); + m_lastDeniabilizationTxHash.reset(); + break; + case TX_UNKNOWN: + LogPrintf("Deniability[%s]: Deniabilization transaction (%s) was not found.\n", m_walletName, m_lastDeniabilizationTxHash.value().GetHex()); + m_lastDeniabilizationTxHash.reset(); + break; + } + } + + Assert(m_deniabilizationFrequency.count() > 0); + uint confirmTarget = m_deniabilizationFrequency.count() / (60 * 10); // 60 seconds per minute, 10 minutes per block + CFeeRate deniabilizationFeeRate = wallet.getDeniabilizationFeeRate(confirmTarget); + + CFeeRate dustRelayFee = m_model->node().getDustRelayFee(); + + // Before we reset the table, keep track of the coin state + for (const auto& coin : m_coinsList) { + // stash the coin state in the state map unless it's already there (eg from load settings) + uint256 coinHash = coin.hash(); + auto mapIter = m_coinStateMap.find(coinHash); + if (mapIter == m_coinStateMap.end()) { + m_coinStateMap[coinHash] = coin.state; + } + } + + m_coinsList.clear(); + + // group UTXOs that share the same scriptPubKey into a CoinInfo + std::map coinInfoMap; + { + auto coinsListMap = wallet.listCoins(); + for (const auto& coinsPair : coinsListMap) { + const auto& coinsTuples = coinsPair.second; + for (const auto& coinTuple : coinsTuples) { + CoinUTXO output; + output.outpoint = std::get<0>(coinTuple); + output.walletTxOut = std::get<1>(coinTuple); + // skip spent outputs + if (output.walletTxOut.is_spent) + continue; + CScript scriptPubKey = output.walletTxOut.txout.scriptPubKey; + if (scriptPubKey.IsUnspendable()) + continue; + auto result = wallet.calculateDeniabilizationCycles(output.outpoint); + output.deniabilizationStats = DeniabilizationStats(result.first, result.second); + coinInfoMap[scriptPubKey].utxos.push_back(std::move(output)); + } + } + } + + // fill in the rest of the CoinInfo data and store into m_coinsList + m_coinsList.reserve(coinInfoMap.size()); + for (const auto& coinInfoPair : coinInfoMap) { + CoinInfo coin = coinInfoPair.second; + Assert(!coin.utxos.empty()); + + // sort the outputs by outpoint so the order matches between runs + auto utxoCompare = [](const CoinUTXO& utxoA, const CoinUTXO& utxoB) -> bool { + return utxoA.outpoint < utxoB.outpoint; + }; + + std::sort(coin.utxos.begin(), coin.utxos.end(), utxoCompare); + + CAmount coinValue = coin.value(); + CScript coinScript = coin.scriptPubKey(); + CAmount dustThreshold = GetDustThreshold(CTxOut(coinValue, coinScript), dustRelayFee); + uint deniabilizationCycles = coin.deniabilizationCycles(); + float deniabilizationProbability = wallet::CalculateDeniabilizationProbability(deniabilizationCycles); + uint deniabilizationProbabilityPercent = deniabilizationProbability * 100; + coin.deniabilizationFeeEstimate = wallet::CalculateDeniabilizationFeeEstimate(coinScript, coinValue, coin.numUTXOs(), deniabilizationCycles, deniabilizationFeeRate); + + coin.state.deniabilizable = Deniabilizable::YES; + if (wallet.privateKeysDisabled() && !hasExternalSigner(wallet)) { + // disable coins that don't have private keys (unless it's an external signer) + coin.state.deniabilizable = Deniabilizable::NO_PRIVATE_KEYS_DISABLED; + } else if (deniabilizationProbabilityPercent == 0) { + // disable coins that are already fully deniabilized + coin.state.deniabilizable = Deniabilizable::NO_FULLY_DENIABILIZED; + } else if (coinValue < coin.deniabilizationFeeEstimate + dustThreshold) { + // disable coins that are too small (eg after full deniabilization won't leave any more than dust) + coin.state.deniabilizable = Deniabilizable::NO_AMOUNT_TOO_SMALL; + } else if (coin.allUTXOsAreBlockReward()) { + // deselect the coin if all UTXOs are from a block reward (thus probably not necessary to deniabilize) + coin.state.deniabilizable = Deniabilizable::YES_BUT_BLOCK_REWARD; + } else if (coin.depthInMainChain() < minChainDepth) { + // deselect non-mature coins + coin.state.deniabilizable = Deniabilizable::YES_BUT_TX_NOT_MATURE; + } else if (coin.anyLockedCoin(wallet)) { + // deselect locked coins + coin.state.deniabilizable = Deniabilizable::YES_BUT_COIN_LOCKED; + } else if (!wallet::IsDeniabilizationWorthwhile(coinValue, coin.deniabilizationFeeEstimate)) { + // deselect coins that are too small to be worth obuscation (eg fees are more than 10% of the amount) + coin.state.deniabilizable = Deniabilizable::YES_BUT_AMOUNT_NOT_WORTHWHILE; + } + + if (coin.state.mayBeDeniabilized()) { + const CoinState* coinState = nullptr; + { + uint256 coinHash = coin.hash(); + auto mapStateIter = m_coinStateMap.find(coinHash); + if (mapStateIter != m_coinStateMap.end()) { + coinState = &mapStateIter->second; + } + } + + if (coinState && coinState->deniabilizable == coin.state.deniabilizable) { + coin.state.checkState = coinState->checkState; + } else { + if (coin.state.canBeDeniabilized()) { + coin.state.checkState = Qt::Checked; + } else { + coin.state.checkState = Qt::Unchecked; + } + } + } else { + coin.state.checkState = Qt::Unchecked; + } + + m_coinsList.push_back(std::move(coin)); + } + + // all state is now transferred to the coin list so we can clear the state map + m_coinStateMap.clear(); + + auto coinCompare = [](const CoinInfo& coinA, const CoinInfo& coinB) -> bool { + // coins that can be deniabilized go first + if (coinA.state.canBeDeniabilized() != coinB.state.canBeDeniabilized()) + return coinA.state.canBeDeniabilized() > coinB.state.canBeDeniabilized(); + // coins that may be deniabilized go first + if (coinA.state.mayBeDeniabilized() != coinB.state.mayBeDeniabilized()) + return coinA.state.mayBeDeniabilized() > coinB.state.mayBeDeniabilized(); + + // calculate a compound "value and probability" and sort larger values first + // this way bigger coins that are more likely to deniabilize will be tried first + CAmount valueProbabilityA = coinA.value() * wallet::CalculateDeniabilizationProbability(coinA.deniabilizationCycles()); + CAmount valueProbabilityB = coinB.value() * wallet::CalculateDeniabilizationProbability(coinB.deniabilizationCycles()); + if (valueProbabilityA != valueProbabilityB) + return valueProbabilityA > valueProbabilityB; + + // coins with more confirmations go first + return coinA.depthInMainChain() > coinB.depthInMainChain(); + }; + + std::sort(m_coinsList.begin(), m_coinsList.end(), coinCompare); + + updateCoinTable(); + updateStart(); + updateStatus(); +} + +bool DeniabilityDialog::signExternalSigner(interfaces::Wallet& wallet, CTransactionRef& tx, const QString& message) +{ + // the wallet must be unlocked before calling this function + Assert(m_model && m_model->getEncryptionStatus() != WalletModel::Locked); + + QMessageBox::StandardButton resultButton = QMessageBox::question(nullptr, tr("Confirm on device"), message, QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel); + if (resultButton == QMessageBox::Cancel) { + // skip all coins to avoid spamming the user + LogPrintf("Deniability[%s]: External signing cancelled.\n", m_walletName); + return false; + } + Assert(resultButton == QMessageBox::Yes); + + CMutableTransaction mtx(*tx); + PartiallySignedTransaction psbtx(mtx); + bool complete = false; + // Always fill without signing first. This prevents an external signer + // from being called prematurely and is not expensive. + std::optional err = wallet.fillPSBT(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete); + Assert(!complete); + Assert(!err); + + try { + err = m_model->wallet().fillPSBT(SIGHASH_ALL, /*sign=*/true, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete); + } catch (const std::runtime_error& e) { + LogPrintf("Deniability[%s]: External sign failed (%s).\n", m_walletName, e.what()); + QMessageBox::critical(nullptr, tr("Sign failed"), e.what()); + return false; + } + if (err == PSBTError::EXTERNAL_SIGNER_NOT_FOUND) { + //: "External signer" means using devices such as hardware wallets. + LogPrintf("Deniability[%s]: External signer not found.\n", m_walletName); + QMessageBox::critical(nullptr, tr("External signer not found"), "External signer not found"); + return false; + } + if (err == PSBTError::EXTERNAL_SIGNER_FAILED) { + //: "External signer" means using devices such as hardware wallets. + LogPrintf("Deniability[%s]: External signer failure.\n", m_walletName); + QMessageBox::critical(nullptr, tr("External signer failure"), "External signer failure"); + return false; + } + if (err) { + LogPrintf("Deniability[%s]: PSBT failure. Failed to create transaction!\n", m_walletName); + QMessageBox::critical(nullptr, tr("PSBT failure"), "Failed to create transaction!"); + return false; + } + // fillPSBT does not always properly finalize + complete = FinalizeAndExtractPSBT(psbtx, mtx); + if (!complete) { + LogPrintf("Deniability[%s]: External signing failed.\n", m_walletName); + return false; + } + // Prepare transaction for broadcast transaction if complete + tx = MakeTransactionRef(mtx); + return true; +} + +void DeniabilityDialog::finalizeTxBroadcast(uint256 hash, CAmount txFee) +{ + // store the transaction hash so we can check up on it later + m_lastDeniabilizationTxHash = hash; + + // update the deniabilization budget with the amount spent on tx fees + Assert(m_deniabilizationBudget >= txFee); + m_deniabilizationBudget -= txFee; + // if the remaining budget is below a single tx fee, then zero it out so the deniabilization process stops + if (m_deniabilizationBudget < txFee) { + m_deniabilizationBudget = 0; + } + m_ui->budgetSpinner->setValue(m_deniabilizationBudget); + Assert(m_deniabilizationBudget == m_ui->budgetSpinner->value()); +} + +DeniabilityDialog::DeniabilizationResult DeniabilityDialog::deniabilizeCoin(CoinInfo coin) +{ + Assert(walletSupportsDeniabilization()); + Assert(coin.state.mayBeDeniabilized()); + Assert(m_deniabilizationTxInProgress); + + // draw a random percent to decide if we should split this coin + // randomizing the split decision makes the deniabilized transaction tree non-uniform and thus harder to identify + uint deniabilizationCycles = coin.deniabilizationCycles(); + float deniabilizationProbability = wallet::CalculateDeniabilizationProbability(deniabilizationCycles); + uint deniabilizationProbabilityPercent = deniabilizationProbability * 100; + Assert(deniabilizationProbabilityPercent > 0); + uint randomPercent = FastRandomContext().randrange(100u); + LogPrintf("Deniability[%s]: Random probability (%u%%), coin probability (%u%%).\n", m_walletName, randomPercent, deniabilizationProbabilityPercent); + if (randomPercent >= deniabilizationProbabilityPercent) { + // skip this coin and retry next cycle + return DENIABILIZATION_SKIP_COIN; + } + + // we need to unlock the wallet to get new addresses and prepare/sign transactions + WalletModel::UnlockContext ctx(m_model->requestUnlock()); + if (!ctx.isValid()) { + // Unlock wallet was cancelled + LogPrintf("Deniability[%s]: Wallet unlock cancelled.\n", m_walletName); + return DENIABILIZATION_SKIP_ALL_COINS; + } + + Wallet& wallet = m_model->wallet(); + + if (coin.anyLockedCoin(wallet)) { + // locked coins are not automatically selected + // so if we got here it means the user manually selected it + // and we can go ahead an unlock it + for (const auto& utxo : coin.utxos) { + if (wallet.isLockedCoin(utxo.outpoint)) { + if (!wallet.unlockCoin(utxo.outpoint)) { + // unlock failed so we'll skip the coin for this cycle + LogPrintf("Deniability[%s]: Coin unlock failed.\n", m_walletName); + return DENIABILIZATION_SKIP_COIN; + } + } + } + } + + Assert(m_deniabilizationFrequency.count() > 0); + uint confirmTarget = m_deniabilizationFrequency.count() / (60 * 10); // 60 seconds per minute, 10 minutes per block + + std::set inputs; + for (const auto& utxo : coin.utxos) { + inputs.insert(utxo.outpoint); + } + + CTransactionRef newTx; + CAmount txFee = 0; + try { + bool sign = !wallet.privateKeysDisabled(); + bool insufficientAmount = false; + auto res = wallet.createDeniabilizationTransaction(inputs, OutputTypeFromDestination(coin.destination()), confirmTarget, deniabilizationCycles, sign, insufficientAmount, txFee); + if (res) { + newTx = *res; + } else if (insufficientAmount) { + // The amount is not enough for a split, so we disable this coin from further deniabilization + LogPrintf("Deniability[%s]: %s\n", m_walletName, util::ErrorString(res).original); + return DENIABILIZATION_DESELECT_COIN; + } else { + LogPrintf("Deniability[%s]: Creating the deniabilization transaction failed (%s).\n", m_walletName, util::ErrorString(res).original); + Q_EMIT message(tr("Deniability"), tr("Creating the deniabilization transaction failed. ") + QString::fromStdString(util::ErrorString(res).translated), CClientUIInterface::MSG_ERROR); + return DENIABILIZATION_STOP; + } + } catch (const std::runtime_error& err) { + // Something unexpected happened, instruct user to report this bug. + LogPrintf("Deniability[%s]: Creating the deniabilization transaction failed (%s).\n", m_walletName, err.what()); + Q_EMIT message(tr("Deniability"), tr("Creating the deniabilization transaction failed. ") + QString::fromStdString(err.what()), CClientUIInterface::MSG_ERROR); + return DENIABILIZATION_STOP; + } + + if (txFee > m_deniabilizationBudget) { + LogPrintf("Deniability[%s]: Deniabilization budget (%d) exhausted (tx fee %d).\n", m_walletName, m_deniabilizationBudget, txFee); + Q_EMIT message(tr("Deniability"), tr("Not enough budget left for a deniabilization transaction!"), CClientUIInterface::MSG_WARNING); + return DENIABILIZATION_STOP; + } + + if (hasExternalSigner(wallet)) { + QString message = tr("Prepare to confirm the deniabilization transaction on your device.

Ready?"); + if (!signExternalSigner(wallet, newTx, message)) { + // skip all coins to avoid spamming the user + return DENIABILIZATION_SKIP_ALL_COINS; + } + } + + // Broadcast the transaction + wallet.commitTransaction(newTx, /*value_map=*/{}, /*order_form=*/{}); + + finalizeTxBroadcast(newTx->GetHash(), txFee); + return DENIABILIZATION_SUCCESS; +} + +bool DeniabilityDialog::bumpDeniabilizationTx(uint256 txid) +{ + Assert(m_model); + WalletModel::UnlockContext ctx(m_model->requestUnlock()); + if (!ctx.isValid()) { + // Unlock wallet was cancelled + LogPrintf("Deniability[%s]: Wallet unlock cancelled.\n", m_walletName); + return false; + } + Wallet& wallet = m_model->wallet(); + + Assert(m_deniabilizationFrequency.count() > 0); + uint confirmTarget = m_deniabilizationFrequency.count() / (60 * 10); // 60 seconds per minute, 10 minutes per block + + CTransactionRef newTx; + CAmount oldTxFee = 0; + CAmount newTxFee = 0; + try { + bool sign = !wallet.privateKeysDisabled(); + auto res = wallet.createBumpDeniabilizationTransaction(txid, confirmTarget, sign, oldTxFee, newTxFee); + if (res) { + newTx = *res; + } else { + LogPrintf("Deniability[%s]: Creating the deniabilization bump transaction failed (%s).\n", m_walletName, util::ErrorString(res).original); + Q_EMIT message(tr("Deniability"), tr("Creating the deniabilization bump transaction failed. ") + QString::fromStdString(util::ErrorString(res).translated), CClientUIInterface::MSG_ERROR); + return false; + } + } catch (const std::runtime_error& err) { + // Something unexpected happened, instruct user to report this bug. + LogPrintf("Deniability[%s]: Creating the deniabilization bump transaction failed (%s).\n", m_walletName, err.what()); + Q_EMIT message(tr("Deniability"), tr("Creating the deniabilization bump transaction failed. ") + QString::fromStdString(err.what()), CClientUIInterface::MSG_ERROR); + return false; + } + + if (newTxFee <= oldTxFee) { + // no point broadcasting a fee bump tx unless it's larger than the old fee + LogPrintf("Deniability[%s]: New tx fee (%d) is not larger than the old tx fee (%d).\n", m_walletName, newTxFee, oldTxFee); + return false; + } + + CAmount txFee = newTxFee - oldTxFee; + if (txFee > m_deniabilizationBudget) { + Q_EMIT message(tr("Deniability"), tr("Not enough budget left for a fee bump!"), CClientUIInterface::MSG_WARNING); + LogPrintf("Deniability[%s]: Not enough budget (%d) for a fee bump (%d).\n", m_walletName, m_deniabilizationBudget, txFee); + return false; + } + + if (hasExternalSigner(wallet)) { + QString message = tr("Prepare to confirm the fee bump of the deniabilization transaction on your device.

Ready?"); + if (!signExternalSigner(wallet, newTx, message)) { + return false; + } + } + + // commit the bumped transaction + std::vector errors; + uint256 new_hash; + if (!wallet.commitBumpTransaction(txid, CMutableTransaction(*newTx), errors, new_hash)) { + LogPrintf("Deniability[%s]: Failed to commit transaction (%s).\n", m_walletName, errors.front().original); + QMessageBox::critical(nullptr, tr("Fee bump error"), tr("Failed to commit transaction") + "
(" + QString::fromStdString(errors.front().translated) + ")"); + return false; + } + + finalizeTxBroadcast(new_hash, txFee); + return true; +} diff --git a/src/qt/deniabilitydialog.h b/src/qt/deniabilitydialog.h new file mode 100644 index 00000000000..f207d7851ee --- /dev/null +++ b/src/qt/deniabilitydialog.h @@ -0,0 +1,190 @@ +// Copyright (c) 2022- The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_DENIABILITYDIALOG_H +#define BITCOIN_QT_DENIABILITYDIALOG_H + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +class ClientModel; +class WalletModel; +class PlatformStyle; +class COutPoint; +class WalletModelTransaction; +enum class SynchronizationState; +enum class SyncType; + +QT_BEGIN_NAMESPACE +class QMenu; +class QPushButton; +class QTimer; +class QTableWidgetItem; +QT_END_NAMESPACE + +namespace Ui { +class DeniabilityDialog; +} + +class DeniabilityDialog : public QDialog +{ + Q_OBJECT + +public: + explicit DeniabilityDialog(const PlatformStyle* platformStyle, QWidget* parent = nullptr); + ~DeniabilityDialog(); + + void setClientModel(ClientModel* model); + void setModel(WalletModel* model); + + bool walletSupportsDeniabilization() const; + bool hasDeniabilizationCandidates() const; + void updateCoins(); + void updateCoinsIfVisible(); + + enum { + destinationRole = Qt::UserRole, + }; + +private Q_SLOTS: + void updateCheckState(QTableWidgetItem* itemCheck); + void startDeniabilization(); + void stopDeniabilization(); + void deniabilizeProc(); + void contextualMenu(const QPoint& point); + void updateNumberOfBlocks(int count, const QDateTime& blockDate, double nVerificationProgress, SyncType synctype, SynchronizationState sync_state); +public Q_SLOTS: + void reject() override; + void accept() override; + +private: + Ui::DeniabilityDialog* m_ui; + QMenu* m_contextMenu = nullptr; + const PlatformStyle* m_platformStyle = nullptr; + + std::string m_walletName; + ClientModel* m_clientModel = nullptr; + WalletModel* m_model = nullptr; + BitcoinUnit m_displayUnit = BitcoinUnit::BTC; + + enum class Deniabilizable : uint { + YES, + YES_BUT_BLOCK_REWARD, + YES_BUT_COIN_LOCKED, + YES_BUT_TX_NOT_MATURE, + YES_BUT_AMOUNT_NOT_WORTHWHILE, + NO_FULLY_DENIABILIZED, + NO_PRIVATE_KEYS_DISABLED, + NO_AMOUNT_TOO_SMALL, + NO, + }; + + struct CoinState { + Deniabilizable deniabilizable = Deniabilizable::NO; + Qt::CheckState checkState = Qt::Unchecked; + inline bool canBeDeniabilized() const + { + return deniabilizable == Deniabilizable::YES; + } + inline bool mayBeDeniabilized() const + { + switch (deniabilizable) { + case Deniabilizable::YES: + case Deniabilizable::YES_BUT_BLOCK_REWARD: + case Deniabilizable::YES_BUT_COIN_LOCKED: + case Deniabilizable::YES_BUT_TX_NOT_MATURE: + case Deniabilizable::YES_BUT_AMOUNT_NOT_WORTHWHILE: + return true; + default: + return false; + } + } + }; + + struct DeniabilizationStats { + uint cycles = 0; + bool blockReward = false; + DeniabilizationStats() = default; + explicit DeniabilizationStats(uint _cycles, bool _blockReward) + : cycles(_cycles), blockReward(_blockReward) + { + } + }; + + struct CoinUTXO { + COutPoint outpoint; + interfaces::WalletTxOut walletTxOut; + DeniabilizationStats deniabilizationStats; + }; + + struct CoinInfo { + CoinState state; + CAmount deniabilizationFeeEstimate = 0; + std::vector utxos; + uint numUTXOs() const + { + return (uint)utxos.size(); + } + CScript scriptPubKey() const; + CTxDestination destination() const; + uint256 hash() const; + CAmount value() const; + int depthInMainChain() const; + uint deniabilizationCycles() const; + bool allUTXOsAreBlockReward() const; + bool anyLockedCoin(interfaces::Wallet& wallet) const; + }; + std::vector m_coinsList; + + std::map m_coinStateMap; + + QTimer* m_deniabilizeProcTimer = nullptr; + bool m_deniabilizationProcessAccepted = false; + bool m_deniabilizationTxInProgress = false; + std::optional m_lastDeniabilizationTxHash; + std::optional m_nextDeniabilizationCycle; + std::chrono::seconds m_deniabilizationFrequency = std::chrono::seconds::zero(); + CAmount m_deniabilizationBudget = 0; + + void clear(); + void setupTableWidget(); + void loadSettings(); + void saveSettings(); + void updateStart(); + void updateStatus(); + void updateCoinTable(); + + bool signExternalSigner(interfaces::Wallet& wallet, CTransactionRef& tx, const QString& message); + void finalizeTxBroadcast(uint256 hash, CAmount txFee); + + enum DeniabilizationResult : uint { + DENIABILIZATION_SUCCESS, + DENIABILIZATION_SKIP_COIN, + DENIABILIZATION_SKIP_ALL_COINS, + DENIABILIZATION_DESELECT_COIN, + DENIABILIZATION_STOP, + }; + + // deniabilize a given coin (passed by value to avoid crashes if m_coinsList gets updated while a tx is being built) + DeniabilizationResult deniabilizeCoin(CoinInfo coin); + + // bump the fee of a deniabilization transaction + bool bumpDeniabilizationTx(uint256 txid); + +Q_SIGNALS: + // Fired when a message should be reported to the user + void message(const QString& title, const QString& message, unsigned int style); +}; + +#endif // BITCOIN_QT_DENIABILITYDIALOG_H diff --git a/src/qt/forms/deniabilitydialog.ui b/src/qt/forms/deniabilitydialog.ui new file mode 100644 index 00000000000..f1ed5d3a08c --- /dev/null +++ b/src/qt/forms/deniabilitydialog.ui @@ -0,0 +1,179 @@ + + + DeniabilityDialog + + + + 0 + 0 + 952 + 808 + + + + Deniability + + + + + + Candidate coins for deniabilization: + + + + + + + + Noto Mono + + + + + + + + + + + + Frequency at which to perform a deniabilization cycle. Less frequent provides better privacy and usually has lower fees. + + + Frequency: + + + + + + + Hourly + + + + + + + Daily + + + true + + + + + + + Weekly + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + -1 + + + 0 + + + + + Total budget to spend on deniabilization transactions + + + Budget: + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + false + + + Start + + + false + + + + + + + false + + + Stop + + + false + + + + + + + + + + + QFrame::StyledPanel + + + + + + + + + + + BitcoinAmountField + QLineEdit +
qt/bitcoinamountfield.h
+ 1 +
+
+ + +
diff --git a/src/qt/res/icons/crosseye.png b/src/qt/res/icons/crosseye.png new file mode 100644 index 0000000000000000000000000000000000000000..a6eb14d38dd714e007dc0f2a5cc359b1881dd3cd GIT binary patch literal 21985 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 literal 0 HcmV?d00001 diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index 6a573d284ca..6fc819aa3b2 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -5,13 +5,13 @@ #include #include -#include #include #include #include #include #include #include +#include #include #include #include @@ -25,8 +25,10 @@ #include #include #include