mirror of
https://github.com/dashpay/dash.git
synced 2024-12-24 19:42:46 +01:00
Merge #6360: backport: bitcoin#11909, #20586, #21090, #21500, #21562, #22232, #22378, #22688, bitcoin-core/gui#381, #390
750447e345
Merge bitcoin/bitcoin#20586: Fix Windows build with --enable-werror (W. J. van der Laan)368a6ef512
Merge bitcoin-core/gui#390: Add SubFeeFromAmount to options (Hennadii Stepanov)7df9788c85
Merge bitcoin-core/gui#381: refactor: Make BitcoinCore class reusable (W. J. van der Laan)40a8b925db
Merge bitcoin/bitcoin#22688: contrib: use `keys.openpgp.org` to retrieve builder keys (fanquake)62b5358a9c
Merge #11909: contrib: Replace developer keys with list of pgp fingerprints (Wladimir J. van der Laan)1ff42b40e3
Merge bitcoin/bitcoin#21500: wallet, rpc: add an option to list private descriptors (Samuel Dobson)5a803ae765
Merge bitcoin/bitcoin#22378: test: remove confusing `MAX_BLOCK_BASE_SIZE` (MarcoFalke)42a0cf0709
Merge bitcoin/bitcoin#21562: [net processing] Various tidying up of PeerManagerImpl ctor (MarcoFalke)e3c69da4f2
Merge bitcoin/bitcoin#22232: refactor: Pass interpreter flags as uint32_t instead of signed int (MarcoFalke) Pull request description: ## What was done? Regular batch of backports from Bitcoin v23 ## How Has This Been Tested? Run unit and functional tests ## Breaking Changes N/A ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone ACKs for top commit: UdjinM6: utACK750447e345
PastaPastaPasta: utACK750447e345
Tree-SHA512: 46e0de22798937b6b0a228ea9ca39b465cd9e8b822f973f23af80fccb1fe2d733716930d3f3843aaf41a58225aa98e18cd5d5e361245f64e146ff1be118f91ca
This commit is contained in:
commit
a0ab06f5c0
@ -25,7 +25,7 @@ test/lint/lint-all.sh
|
||||
|
||||
if [ "$CIRRUS_REPO_FULL_NAME" = "dashpay/dash" ] && [ -n "$CIRRUS_CRON" ]; then
|
||||
git log --merges --before="2 days ago" -1 --format='%H' > ./contrib/verify-commits/trusted-sha512-root-commit
|
||||
${CI_RETRY_EXE} gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys $(<contrib/verify-commits/trusted-keys) &&
|
||||
${CI_RETRY_EXE} gpg --keyserver hkps://keys.openpgp.org --recv-keys $(<contrib/verify-commits/trusted-keys) &&
|
||||
./contrib/verify-commits/verify-commits.py --clean-merge=2;
|
||||
fi
|
||||
|
||||
|
@ -15,7 +15,3 @@ export RUN_SECURITY_TESTS="false"
|
||||
export GOAL="deploy"
|
||||
export BITCOIN_CONFIG="--enable-gui --enable-reduce-exports --disable-miner --without-boost-process"
|
||||
export DIRECT_WINE_EXEC_TESTS=true
|
||||
|
||||
# Compiler for MinGW-w64 causes false -Wreturn-type warning.
|
||||
# See https://sourceforge.net/p/mingw-w64/bugs/306/
|
||||
export NO_WERROR=1
|
||||
|
@ -459,7 +459,13 @@ if test "x$enable_werror" = "xyes"; then
|
||||
AX_CHECK_COMPILE_FLAG([-Werror=range-loop-analysis],[ERROR_CXXFLAGS="$ERROR_CXXFLAGS -Werror=range-loop-analysis"],,[[$CXXFLAG_WERROR]])
|
||||
AX_CHECK_COMPILE_FLAG([-Werror=unused-variable],[ERROR_CXXFLAGS="$ERROR_CXXFLAGS -Werror=unused-variable"],,[[$CXXFLAG_WERROR]])
|
||||
AX_CHECK_COMPILE_FLAG([-Werror=date-time],[ERROR_CXXFLAGS="$ERROR_CXXFLAGS -Werror=date-time"],,[[$CXXFLAG_WERROR]])
|
||||
AX_CHECK_COMPILE_FLAG([-Werror=return-type],[ERROR_CXXFLAGS="$ERROR_CXXFLAGS -Werror=return-type"],,[[$CXXFLAG_WERROR]])
|
||||
|
||||
dnl -Wreturn-type is broken in GCC for MinGW-w64.
|
||||
dnl https://sourceforge.net/p/mingw-w64/bugs/306/
|
||||
AX_CHECK_COMPILE_FLAG([-Werror=return-type], [ERROR_CXXFLAGS="$ERROR_CXXFLAGS -Werror=return-type"], [], [$CXXFLAG_WERROR],
|
||||
[AC_LANG_SOURCE([[#include <cassert>
|
||||
int f(){ assert(false); }]])])
|
||||
|
||||
AX_CHECK_COMPILE_FLAG([-Werror=conditional-uninitialized],[ERROR_CXXFLAGS="$ERROR_CXXFLAGS -Werror=conditional-uninitialized"],,[[$CXXFLAG_WERROR]])
|
||||
AX_CHECK_COMPILE_FLAG([-Werror=sign-compare],[ERROR_CXXFLAGS="$ERROR_CXXFLAGS -Werror=sign-compare"],,[[$CXXFLAG_WERROR]])
|
||||
dnl -Wsuggest-override is broken with GCC before 9.2
|
||||
|
@ -1,15 +1,26 @@
|
||||
PGP keys
|
||||
========
|
||||
## PGP keys of builders and Developers
|
||||
|
||||
This folder contains the public keys of developers and active contributors.
|
||||
The file `keys.txt` contains fingerprints of the public keys of builders and
|
||||
active developers.
|
||||
|
||||
The keys are mainly used to sign git commits or the build results of builds.
|
||||
|
||||
You can import the keys into gpg as follows. Also, make sure to fetch the
|
||||
latest version from the key server to see if any key was revoked in the
|
||||
meantime.
|
||||
The most recent version of each pgp key can be found on most pgp key servers.
|
||||
|
||||
Fetch the latest version from the key server to see if any key was revoked in
|
||||
the meantime.
|
||||
To fetch the latest version of all pgp keys in your gpg homedir,
|
||||
|
||||
```sh
|
||||
gpg --import ./*.pgp
|
||||
gpg --refresh-keys
|
||||
```
|
||||
|
||||
To fetch keys of builders and active developers, feed the list of fingerprints
|
||||
of the primary keys into gpg:
|
||||
|
||||
```sh
|
||||
while read fingerprint keyholder_name; do gpg --keyserver hkps://keys.openpgp.org --recv-keys ${fingerprint}; done < ./keys.txt
|
||||
```
|
||||
|
||||
Add your key to the list if you provided Guix attestations for two major or
|
||||
minor releases of Dash Core.
|
||||
|
@ -40,7 +40,7 @@ Import trusted keys
|
||||
In order to check the commit signatures, you must add the trusted PGP keys to your machine. [GnuPG](https://gnupg.org/) may be used to import the trusted keys by running the following command:
|
||||
|
||||
```sh
|
||||
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys $(<contrib/verify-commits/trusted-keys)
|
||||
gpg --keyserver hkps://keys.openpgp.org --recv-keys $(<contrib/verify-commits/trusted-keys)
|
||||
```
|
||||
|
||||
Key expiry/revocation
|
||||
|
@ -58,6 +58,7 @@ QT_MOC_CPP = \
|
||||
qt/moc_editaddressdialog.cpp \
|
||||
qt/moc_governancelist.cpp \
|
||||
qt/moc_guiutil.cpp \
|
||||
qt/moc_initexecutor.cpp \
|
||||
qt/moc_intro.cpp \
|
||||
qt/moc_macdockiconhandler.cpp \
|
||||
qt/moc_macnotificationhandler.cpp \
|
||||
@ -133,6 +134,7 @@ BITCOIN_QT_H = \
|
||||
qt/governancelist.h \
|
||||
qt/guiconstants.h \
|
||||
qt/guiutil.h \
|
||||
qt/initexecutor.h \
|
||||
qt/intro.h \
|
||||
qt/macdockiconhandler.h \
|
||||
qt/macnotificationhandler.h \
|
||||
@ -222,6 +224,7 @@ BITCOIN_QT_BASE_CPP = \
|
||||
qt/clientmodel.cpp \
|
||||
qt/csvmodelwriter.cpp \
|
||||
qt/guiutil.cpp \
|
||||
qt/initexecutor.cpp \
|
||||
qt/intro.cpp \
|
||||
qt/modaloverlay.cpp \
|
||||
qt/networkstyle.cpp \
|
||||
|
@ -49,7 +49,7 @@ unsigned int GetP2SHSigOpCount(const CTransaction& tx, const CCoinsViewCache& ma
|
||||
* Count total signature operations for a transaction.
|
||||
* @param[in] tx Transaction for which we are counting sigops
|
||||
* @param[in] inputs Map of previous transactions that have outputs we're spending
|
||||
* @param[out] flags Script verification flags
|
||||
* @param[in] flags Script verification flags
|
||||
* @return Total signature operation count for a tx
|
||||
*/
|
||||
unsigned int GetTransactionSigOpCount(const CTransaction& tx, const CCoinsViewCache& inputs, uint32_t flags);
|
||||
|
@ -872,7 +872,7 @@ private:
|
||||
* million to make it highly unlikely for users to have issues with this
|
||||
* filter.
|
||||
*
|
||||
* Memory used: 1.3MB
|
||||
* Memory used: 1.3 MB
|
||||
*/
|
||||
CRollingBloomFilter m_recent_rejects GUARDED_BY(::cs_main){120'000, 0.000'001};
|
||||
uint256 hashRecentRejectsChainTip GUARDED_BY(cs_main);
|
||||
@ -1620,6 +1620,7 @@ void PeerManagerImpl::FinalizeNode(const CNode& node) {
|
||||
assert(m_num_preferred_download_peers == 0);
|
||||
assert(m_peers_downloading_from == 0);
|
||||
assert(m_outbound_peers_with_protect_from_disconnect == 0);
|
||||
assert(m_orphanage.Size() == 0);
|
||||
}
|
||||
} // cs_main
|
||||
|
||||
|
@ -8,21 +8,34 @@
|
||||
#endif
|
||||
|
||||
#include <qt/bitcoin.h>
|
||||
#include <qt/bitcoingui.h>
|
||||
|
||||
#include <chainparams.h>
|
||||
#include <fs.h>
|
||||
#include <init.h>
|
||||
#include <interfaces/handler.h>
|
||||
#include <interfaces/node.h>
|
||||
#include <net.h>
|
||||
#include <node/context.h>
|
||||
#include <node/ui_interface.h>
|
||||
#include <noui.h>
|
||||
#include <qt/bitcoingui.h>
|
||||
#include <qt/clientmodel.h>
|
||||
#include <qt/guiconstants.h>
|
||||
#include <qt/guiutil.h>
|
||||
#include <qt/initexecutor.h>
|
||||
#include <qt/intro.h>
|
||||
#include <net.h>
|
||||
#include <qt/networkstyle.h>
|
||||
#include <qt/optionsmodel.h>
|
||||
#include <qt/splashscreen.h>
|
||||
#include <qt/utilitydialog.h>
|
||||
#include <qt/winshutdownmonitor.h>
|
||||
#include <stacktraces.h>
|
||||
#include <uint256.h>
|
||||
#include <util/string.h>
|
||||
#include <util/system.h>
|
||||
#include <util/threadnames.h>
|
||||
#include <util/translation.h>
|
||||
#include <validation.h>
|
||||
|
||||
#ifdef ENABLE_WALLET
|
||||
#include <qt/paymentserver.h>
|
||||
@ -30,19 +43,6 @@
|
||||
#include <qt/walletmodel.h>
|
||||
#endif // ENABLE_WALLET
|
||||
|
||||
#include <init.h>
|
||||
#include <interfaces/handler.h>
|
||||
#include <interfaces/node.h>
|
||||
#include <node/context.h>
|
||||
#include <node/ui_interface.h>
|
||||
#include <noui.h>
|
||||
#include <stacktraces.h>
|
||||
#include <uint256.h>
|
||||
#include <util/system.h>
|
||||
#include <util/threadnames.h>
|
||||
#include <util/translation.h>
|
||||
#include <validation.h>
|
||||
|
||||
#include <boost/signals2/connection.hpp>
|
||||
#include <memory>
|
||||
|
||||
@ -52,7 +52,6 @@
|
||||
#include <QLibraryInfo>
|
||||
#include <QLocale>
|
||||
#include <QMessageBox>
|
||||
#include <QProcess>
|
||||
#include <QSettings>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
@ -208,72 +207,11 @@ void DebugMessageHandler(QtMsgType type, const QMessageLogContext& context, cons
|
||||
}
|
||||
}
|
||||
|
||||
BitcoinCore::BitcoinCore(interfaces::Node& node) :
|
||||
QObject(), m_node(node)
|
||||
{
|
||||
}
|
||||
|
||||
void BitcoinCore::handleRunawayException(const std::exception_ptr e)
|
||||
{
|
||||
PrintExceptionContinue(e, "Runaway exception");
|
||||
Q_EMIT runawayException(QString::fromStdString(m_node.getWarnings().translated));
|
||||
}
|
||||
|
||||
void BitcoinCore::initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
util::ThreadRename("qt-init");
|
||||
qDebug() << __func__ << ": Running initialization in thread";
|
||||
interfaces::BlockAndHeaderTipInfo tip_info;
|
||||
bool rv = m_node.appInitMain(&tip_info);
|
||||
Q_EMIT initializeResult(rv, tip_info);
|
||||
} catch (...) {
|
||||
handleRunawayException(std::current_exception());
|
||||
}
|
||||
}
|
||||
|
||||
void BitcoinCore::restart(QStringList args)
|
||||
{
|
||||
static bool executing_restart{false};
|
||||
|
||||
if(!executing_restart) { // Only restart 1x, no matter how often a user clicks on a restart-button
|
||||
executing_restart = true;
|
||||
try
|
||||
{
|
||||
qDebug() << __func__ << ": Running Restart in thread";
|
||||
m_node.appPrepareShutdown();
|
||||
qDebug() << __func__ << ": Shutdown finished";
|
||||
Q_EMIT shutdownResult();
|
||||
CExplicitNetCleanup::callCleanup();
|
||||
QProcess::startDetached(QApplication::applicationFilePath(), args);
|
||||
qDebug() << __func__ << ": Restart initiated...";
|
||||
QApplication::quit();
|
||||
} catch (...) {
|
||||
handleRunawayException(std::current_exception());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BitcoinCore::shutdown()
|
||||
{
|
||||
try
|
||||
{
|
||||
qDebug() << __func__ << ": Running Shutdown in thread";
|
||||
m_node.appShutdown();
|
||||
qDebug() << __func__ << ": Shutdown finished";
|
||||
Q_EMIT shutdownResult();
|
||||
} catch (...) {
|
||||
handleRunawayException(std::current_exception());
|
||||
}
|
||||
}
|
||||
|
||||
static int qt_argc = 1;
|
||||
static const char* qt_argv = "dash-qt";
|
||||
|
||||
BitcoinApplication::BitcoinApplication():
|
||||
QApplication(qt_argc, const_cast<char **>(&qt_argv)),
|
||||
coreThread(nullptr),
|
||||
optionsModel(nullptr),
|
||||
clientModel(nullptr),
|
||||
window(nullptr),
|
||||
@ -287,13 +225,7 @@ BitcoinApplication::BitcoinApplication():
|
||||
|
||||
BitcoinApplication::~BitcoinApplication()
|
||||
{
|
||||
if(coreThread)
|
||||
{
|
||||
qDebug() << __func__ << ": Stopping thread";
|
||||
coreThread->quit();
|
||||
coreThread->wait();
|
||||
qDebug() << __func__ << ": Stopped thread";
|
||||
}
|
||||
m_executor.reset();
|
||||
|
||||
delete window;
|
||||
window = nullptr;
|
||||
@ -350,23 +282,16 @@ bool BitcoinApplication::baseInitialize()
|
||||
|
||||
void BitcoinApplication::startThread()
|
||||
{
|
||||
if(coreThread)
|
||||
return;
|
||||
coreThread = new QThread(this);
|
||||
BitcoinCore *executor = new BitcoinCore(node());
|
||||
executor->moveToThread(coreThread);
|
||||
assert(!m_executor);
|
||||
m_executor.emplace(node());
|
||||
|
||||
/* communication to and from thread */
|
||||
connect(executor, &BitcoinCore::initializeResult, this, &BitcoinApplication::initializeResult);
|
||||
connect(executor, &BitcoinCore::shutdownResult, this, &BitcoinApplication::shutdownResult);
|
||||
connect(executor, &BitcoinCore::runawayException, this, &BitcoinApplication::handleRunawayException);
|
||||
connect(this, &BitcoinApplication::requestedInitialize, executor, &BitcoinCore::initialize);
|
||||
connect(this, &BitcoinApplication::requestedShutdown, executor, &BitcoinCore::shutdown);
|
||||
connect(window, &BitcoinGUI::requestedRestart, executor, &BitcoinCore::restart);
|
||||
/* make sure executor object is deleted in its own thread */
|
||||
connect(coreThread, &QThread::finished, executor, &QObject::deleteLater);
|
||||
|
||||
coreThread->start();
|
||||
connect(&m_executor.value(), &InitExecutor::initializeResult, this, &BitcoinApplication::initializeResult);
|
||||
connect(&m_executor.value(), &InitExecutor::shutdownResult, this, &BitcoinApplication::shutdownResult);
|
||||
connect(&m_executor.value(), &InitExecutor::runawayException, this, &BitcoinApplication::handleRunawayException);
|
||||
connect(this, &BitcoinApplication::requestedInitialize, &m_executor.value(), &InitExecutor::initialize);
|
||||
connect(this, &BitcoinApplication::requestedShutdown, &m_executor.value(), &InitExecutor::shutdown);
|
||||
connect(window, &BitcoinGUI::requestedRestart, &m_executor.value(), &InitExecutor::restart);
|
||||
}
|
||||
|
||||
void BitcoinApplication::parameterSetup()
|
||||
@ -399,7 +324,6 @@ void BitcoinApplication::requestShutdown()
|
||||
shutdownWindow.reset(ShutdownWindow::showShutdownWindow(window));
|
||||
|
||||
qDebug() << __func__ << ": Requesting shutdown";
|
||||
startThread();
|
||||
window->hide();
|
||||
// Must disconnect node signals otherwise current thread can deadlock since
|
||||
// no event loop is running.
|
||||
|
@ -9,11 +9,14 @@
|
||||
#include <config/bitcoin-config.h>
|
||||
#endif
|
||||
|
||||
#include <QApplication>
|
||||
#include <interfaces/node.h>
|
||||
#include <qt/initexecutor.h>
|
||||
|
||||
#include <assert.h>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include <interfaces/node.h>
|
||||
#include <QApplication>
|
||||
|
||||
class BitcoinGUI;
|
||||
class ClientModel;
|
||||
@ -25,32 +28,6 @@ class WalletController;
|
||||
class WalletModel;
|
||||
|
||||
|
||||
/** Class encapsulating Bitcoin Core startup and shutdown.
|
||||
* Allows running startup and shutdown in a different thread from the UI thread.
|
||||
*/
|
||||
class BitcoinCore: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit BitcoinCore(interfaces::Node& node);
|
||||
|
||||
public Q_SLOTS:
|
||||
void initialize();
|
||||
void shutdown();
|
||||
void restart(QStringList args);
|
||||
|
||||
Q_SIGNALS:
|
||||
void initializeResult(bool success, interfaces::BlockAndHeaderTipInfo tip_info);
|
||||
void shutdownResult();
|
||||
void runawayException(const QString &message);
|
||||
|
||||
private:
|
||||
/// Pass fatal exception message to UI thread
|
||||
void handleRunawayException(const std::exception_ptr e);
|
||||
|
||||
interfaces::Node& m_node;
|
||||
};
|
||||
|
||||
/** Main Bitcoin application object */
|
||||
class BitcoinApplication: public QApplication
|
||||
{
|
||||
@ -113,7 +90,7 @@ protected:
|
||||
bool event(QEvent* e) override;
|
||||
|
||||
private:
|
||||
QThread *coreThread;
|
||||
std::optional<InitExecutor> m_executor;
|
||||
OptionsModel *optionsModel;
|
||||
ClientModel *clientModel;
|
||||
BitcoinGUI *window;
|
||||
|
@ -315,6 +315,16 @@
|
||||
</widget>
|
||||
<widget class="QWidget" name="pageWallet">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_Wallet">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="subFeeFromAmount">
|
||||
<property name="toolTip">
|
||||
<string extracomment="Tooltip text for Options window setting that sets subtracting the fee from a sending amount as default.">Whether to set subtract fee from amount as default or not.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string extracomment="An Options window setting to set subtracting the fee from a sending amount as default.">Subtract &fee from amount by default</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
|
85
src/qt/initexecutor.cpp
Normal file
85
src/qt/initexecutor.cpp
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2014-2021 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 <qt/initexecutor.h>
|
||||
|
||||
#include <interfaces/node.h>
|
||||
#include <util/system.h>
|
||||
#include <util/threadnames.h>
|
||||
|
||||
#include <exception>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QString>
|
||||
#include <QThread>
|
||||
|
||||
InitExecutor::InitExecutor(interfaces::Node& node)
|
||||
: QObject(), m_node(node)
|
||||
{
|
||||
this->moveToThread(&m_thread);
|
||||
m_thread.start();
|
||||
}
|
||||
|
||||
InitExecutor::~InitExecutor()
|
||||
{
|
||||
qDebug() << __func__ << ": Stopping thread";
|
||||
m_thread.quit();
|
||||
m_thread.wait();
|
||||
qDebug() << __func__ << ": Stopped thread";
|
||||
}
|
||||
|
||||
void InitExecutor::handleRunawayException(const std::exception_ptr e)
|
||||
{
|
||||
PrintExceptionContinue(e, "Runaway exception");
|
||||
Q_EMIT runawayException(QString::fromStdString(m_node.getWarnings().translated));
|
||||
}
|
||||
|
||||
void InitExecutor::initialize()
|
||||
{
|
||||
try {
|
||||
util::ThreadRename("qt-init");
|
||||
qDebug() << __func__ << ": Running initialization in thread";
|
||||
interfaces::BlockAndHeaderTipInfo tip_info;
|
||||
bool rv = m_node.appInitMain(&tip_info);
|
||||
Q_EMIT initializeResult(rv, tip_info);
|
||||
} catch (...) {
|
||||
handleRunawayException(std::current_exception());
|
||||
}
|
||||
}
|
||||
|
||||
void InitExecutor::restart(QStringList args)
|
||||
{
|
||||
static bool executing_restart{false};
|
||||
|
||||
if(!executing_restart) { // Only restart 1x, no matter how often a user clicks on a restart-button
|
||||
executing_restart = true;
|
||||
try {
|
||||
qDebug() << __func__ << ": Running Restart in thread";
|
||||
m_node.appPrepareShutdown();
|
||||
qDebug() << __func__ << ": Shutdown finished";
|
||||
Q_EMIT shutdownResult();
|
||||
CExplicitNetCleanup::callCleanup();
|
||||
QProcess::startDetached(QApplication::applicationFilePath(), args);
|
||||
qDebug() << __func__ << ": Restart initiated...";
|
||||
QApplication::quit();
|
||||
} catch (...) {
|
||||
handleRunawayException(std::current_exception());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InitExecutor::shutdown()
|
||||
{
|
||||
try {
|
||||
qDebug() << __func__ << ": Running Shutdown in thread";
|
||||
m_node.appShutdown();
|
||||
qDebug() << __func__ << ": Shutdown finished";
|
||||
Q_EMIT shutdownResult();
|
||||
} catch (...) {
|
||||
handleRunawayException(std::current_exception());
|
||||
}
|
||||
}
|
47
src/qt/initexecutor.h
Normal file
47
src/qt/initexecutor.h
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2014-2021 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_INITEXECUTOR_H
|
||||
#define BITCOIN_QT_INITEXECUTOR_H
|
||||
|
||||
#include <interfaces/node.h>
|
||||
|
||||
#include <exception>
|
||||
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QString;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
/** Class encapsulating Bitcoin Core startup and shutdown.
|
||||
* Allows running startup and shutdown in a different thread from the UI thread.
|
||||
*/
|
||||
class InitExecutor : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit InitExecutor(interfaces::Node& node);
|
||||
~InitExecutor();
|
||||
|
||||
public Q_SLOTS:
|
||||
void initialize();
|
||||
void shutdown();
|
||||
void restart(QStringList args);
|
||||
|
||||
Q_SIGNALS:
|
||||
void initializeResult(bool success, interfaces::BlockAndHeaderTipInfo tip_info);
|
||||
void shutdownResult();
|
||||
void runawayException(const QString& message);
|
||||
|
||||
private:
|
||||
/// Pass fatal exception message to UI thread
|
||||
void handleRunawayException(const std::exception_ptr e);
|
||||
|
||||
interfaces::Node& m_node;
|
||||
QThread m_thread;
|
||||
};
|
||||
|
||||
#endif // BITCOIN_QT_INITEXECUTOR_H
|
@ -325,6 +325,7 @@ void OptionsDialog::setMapper()
|
||||
|
||||
/* Wallet */
|
||||
mapper->addMapping(ui->coinControlFeatures, OptionsModel::CoinControlFeatures);
|
||||
mapper->addMapping(ui->subFeeFromAmount, OptionsModel::SubFeeFromAmount);
|
||||
mapper->addMapping(ui->keepChangeAddress, OptionsModel::KeepChangeAddress);
|
||||
mapper->addMapping(ui->showMasternodesTab, OptionsModel::ShowMasternodesTab);
|
||||
mapper->addMapping(ui->showGovernanceTab, OptionsModel::ShowGovernanceTab);
|
||||
|
@ -222,6 +222,11 @@ void OptionsModel::Init(bool resetSettings)
|
||||
if (!gArgs.SoftSetBoolArg("-spendzeroconfchange", settings.value("bSpendZeroConfChange").toBool()))
|
||||
addOverriddenOption("-spendzeroconfchange");
|
||||
|
||||
if (!settings.contains("SubFeeFromAmount")) {
|
||||
settings.setValue("SubFeeFromAmount", false);
|
||||
}
|
||||
m_sub_fee_from_amount = settings.value("SubFeeFromAmount", false).toBool();
|
||||
|
||||
// CoinJoin
|
||||
if (!settings.contains("nCoinJoinSessions"))
|
||||
settings.setValue("nCoinJoinSessions", DEFAULT_COINJOIN_SESSIONS);
|
||||
@ -458,6 +463,8 @@ QVariant OptionsModel::data(const QModelIndex & index, int role) const
|
||||
#ifdef ENABLE_WALLET
|
||||
case SpendZeroConfChange:
|
||||
return settings.value("bSpendZeroConfChange");
|
||||
case SubFeeFromAmount:
|
||||
return m_sub_fee_from_amount;
|
||||
case ShowMasternodesTab:
|
||||
return settings.value("fShowMasternodesTab");
|
||||
case ShowGovernanceTab:
|
||||
@ -633,6 +640,10 @@ bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, in
|
||||
setRestartRequired(true);
|
||||
}
|
||||
break;
|
||||
case SubFeeFromAmount:
|
||||
m_sub_fee_from_amount = value.toBool();
|
||||
settings.setValue("SubFeeFromAmount", m_sub_fee_from_amount);
|
||||
break;
|
||||
case ShowGovernanceTab:
|
||||
if (settings.value("fShowGovernanceTab") != value) {
|
||||
settings.setValue("fShowGovernanceTab", value);
|
||||
|
@ -67,6 +67,7 @@ public:
|
||||
FontWeightBold, // int
|
||||
Language, // QString
|
||||
CoinControlFeatures, // bool
|
||||
SubFeeFromAmount, // bool
|
||||
KeepChangeAddress, // bool
|
||||
ThreadsScriptVerif, // int
|
||||
Prune, // bool
|
||||
@ -105,6 +106,7 @@ public:
|
||||
int getDisplayUnit() const { return nDisplayUnit; }
|
||||
QString getThirdPartyTxUrls() const { return strThirdPartyTxUrls; }
|
||||
bool getCoinControlFeatures() const { return fCoinControlFeatures; }
|
||||
bool getSubFeeFromAmount() const { return m_sub_fee_from_amount; }
|
||||
bool getKeepChangeAddress() const { return fKeepChangeAddress; }
|
||||
bool getShowAdvancedCJUI() { return fShowAdvancedCJUI; }
|
||||
const QString& getOverriddenByCommandLine() { return strOverriddenByCommandLine; }
|
||||
@ -132,6 +134,7 @@ private:
|
||||
int nDisplayUnit;
|
||||
QString strThirdPartyTxUrls;
|
||||
bool fCoinControlFeatures;
|
||||
bool m_sub_fee_from_amount;
|
||||
bool fKeepChangeAddress;
|
||||
bool fShowAdvancedCJUI;
|
||||
/* settings that were overridden by command-line */
|
||||
|
@ -103,7 +103,9 @@ void SendCoinsEntry::clear()
|
||||
ui->payTo->clear();
|
||||
ui->addAsLabel->clear();
|
||||
ui->payAmount->clear();
|
||||
ui->checkboxSubtractFeeFromAmount->setCheckState(Qt::Unchecked);
|
||||
if (model && model->getOptionsModel()) {
|
||||
ui->checkboxSubtractFeeFromAmount->setChecked(model->getOptionsModel()->getSubFeeFromAmount());
|
||||
}
|
||||
ui->messageTextLabel->clear();
|
||||
ui->messageTextLabel->hide();
|
||||
ui->messageLabel->hide();
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
#include <interfaces/node.h>
|
||||
#include <qt/bitcoin.h>
|
||||
#include <qt/initexecutor.h>
|
||||
#include <qt/test/apptests.h>
|
||||
#include <qt/test/rpcnestedtests.h>
|
||||
#include <qt/test/uritests.h>
|
||||
|
@ -17,7 +17,7 @@ class WinShutdownMonitor : public QAbstractNativeEventFilter
|
||||
{
|
||||
public:
|
||||
/** Implements QAbstractNativeEventFilter interface for processing Windows messages */
|
||||
bool nativeEventFilter(const QByteArray &eventType, void *pMessage, long *pnResult);
|
||||
bool nativeEventFilter(const QByteArray &eventType, void *pMessage, long *pnResult) override;
|
||||
|
||||
/** Register the reason for blocking shutdown on Windows to allow clean client exit */
|
||||
static void registerShutdownBlockReason(const QString& strReason, const HWND& mainWinId);
|
||||
|
@ -164,6 +164,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
||||
{ "importmulti", 0, "requests" },
|
||||
{ "importmulti", 1, "options" },
|
||||
{ "importdescriptors", 0, "requests" },
|
||||
{ "listdescriptors", 0, "private" },
|
||||
{ "verifychain", 0, "checklevel" },
|
||||
{ "verifychain", 1, "nblocks" },
|
||||
{ "getblockstats", 0, "hash_or_height" },
|
||||
|
@ -50,6 +50,13 @@ public:
|
||||
* (ie orphans that may have found their final missing parent, and so should be reconsidered for the mempool) */
|
||||
void AddChildrenToWorkSet(const CTransaction& tx, std::set<uint256>& orphan_work_set) const EXCLUSIVE_LOCKS_REQUIRED(g_cs_orphans);
|
||||
|
||||
/** Return how many entries exist in the orphange */
|
||||
size_t Size() LOCKS_EXCLUDED(::g_cs_orphans)
|
||||
{
|
||||
LOCK(::g_cs_orphans);
|
||||
return m_orphans.size();
|
||||
}
|
||||
|
||||
protected:
|
||||
struct OrphanTx {
|
||||
CTransactionRef tx;
|
||||
|
@ -1941,8 +1941,10 @@ RPCHelpMan listdescriptors()
|
||||
{
|
||||
return RPCHelpMan{
|
||||
"listdescriptors",
|
||||
"\nList descriptors imported into a descriptor-enabled wallet.",
|
||||
{},
|
||||
"\nList descriptors imported into a descriptor-enabled wallet.\n",
|
||||
{
|
||||
{"private", RPCArg::Type::BOOL, RPCArg::Default{false}, "Show private descriptors."}
|
||||
},
|
||||
RPCResult{RPCResult::Type::OBJ, "", "", {
|
||||
{RPCResult::Type::STR, "wallet_name", "Name of wallet this operation was performed on"},
|
||||
{RPCResult::Type::ARR, "descriptors", "Array of descriptor objects",
|
||||
@ -1962,6 +1964,7 @@ RPCHelpMan listdescriptors()
|
||||
}},
|
||||
RPCExamples{
|
||||
HelpExampleCli("listdescriptors", "") + HelpExampleRpc("listdescriptors", "")
|
||||
+ HelpExampleCli("listdescriptors", "true") + HelpExampleRpc("listdescriptors", "true")
|
||||
},
|
||||
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
||||
{
|
||||
@ -1972,6 +1975,11 @@ RPCHelpMan listdescriptors()
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "listdescriptors is not available for non-descriptor wallets");
|
||||
}
|
||||
|
||||
const bool priv = !request.params[0].isNull() && request.params[0].get_bool();
|
||||
if (priv) {
|
||||
EnsureWalletIsUnlocked(*wallet);
|
||||
}
|
||||
|
||||
LOCK(wallet->cs_wallet);
|
||||
|
||||
UniValue descriptors(UniValue::VARR);
|
||||
@ -1985,8 +1993,9 @@ RPCHelpMan listdescriptors()
|
||||
LOCK(desc_spk_man->cs_desc_man);
|
||||
const auto& wallet_descriptor = desc_spk_man->GetWalletDescriptor();
|
||||
std::string descriptor;
|
||||
if (!desc_spk_man->GetDescriptorString(descriptor)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Can't get normalized descriptor string.");
|
||||
|
||||
if (!desc_spk_man->GetDescriptorString(descriptor, priv)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Can't get descriptor string.");
|
||||
}
|
||||
spk.pushKV("desc", descriptor);
|
||||
spk.pushKV("timestamp", wallet_descriptor.creation_time);
|
||||
|
@ -4009,7 +4009,7 @@ RPCHelpMan getaddressinfo()
|
||||
DescriptorScriptPubKeyMan* desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(pwallet->GetScriptPubKeyMan(scriptPubKey));
|
||||
if (desc_spk_man) {
|
||||
std::string desc_str;
|
||||
if (desc_spk_man->GetDescriptorString(desc_str)) {
|
||||
if (desc_spk_man->GetDescriptorString(desc_str, /* priv */ false)) {
|
||||
ret.pushKV("parent_desc", desc_str);
|
||||
}
|
||||
}
|
||||
|
@ -2403,13 +2403,20 @@ const std::vector<CScript> DescriptorScriptPubKeyMan::GetScriptPubKeys() const
|
||||
return script_pub_keys;
|
||||
}
|
||||
|
||||
bool DescriptorScriptPubKeyMan::GetDescriptorString(std::string& out) const
|
||||
bool DescriptorScriptPubKeyMan::GetDescriptorString(std::string& out, const bool priv) const
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
|
||||
FlatSigningProvider provider;
|
||||
provider.keys = GetKeys();
|
||||
|
||||
if (priv) {
|
||||
// For the private version, always return the master key to avoid
|
||||
// exposing child private keys. The risk implications of exposing child
|
||||
// private keys together with the parent xpub may be non-obvious for users.
|
||||
return m_wallet_descriptor.descriptor->ToPrivateString(provider, out);
|
||||
}
|
||||
|
||||
return m_wallet_descriptor.descriptor->ToNormalizedString(provider, out, &m_wallet_descriptor.cache);
|
||||
}
|
||||
|
||||
|
@ -600,7 +600,7 @@ public:
|
||||
const WalletDescriptor GetWalletDescriptor() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
|
||||
const std::vector<CScript> GetScriptPubKeys() const;
|
||||
|
||||
bool GetDescriptorString(std::string& out) const;
|
||||
bool GetDescriptorString(std::string& out, const bool priv) const;
|
||||
|
||||
void UpgradeDescriptorCache();
|
||||
};
|
||||
|
@ -329,24 +329,24 @@ class FullBlockTest(BitcoinTestFramework):
|
||||
self.move_tip(15)
|
||||
b23 = self.next_block(23, spend=out[6])
|
||||
tx = CTransaction()
|
||||
script_length = MAX_BLOCK_SIZE - len(b23.serialize()) - 69
|
||||
script_length = MAX_BLOCK_SIZE - b23.get_weight() - 69
|
||||
script_output = CScript([b'\x00' * script_length])
|
||||
tx.vout.append(CTxOut(0, script_output))
|
||||
tx.vin.append(CTxIn(COutPoint(b23.vtx[1].sha256, 0)))
|
||||
b23 = self.update_block(23, [tx])
|
||||
# Make sure the math above worked out to produce a max-sized block
|
||||
assert_equal(len(b23.serialize()), MAX_BLOCK_SIZE)
|
||||
assert_equal(b23.get_weight(), MAX_BLOCK_SIZE)
|
||||
self.send_blocks([b23], True)
|
||||
self.save_spendable_output()
|
||||
|
||||
self.log.info("Reject a block of size MAX_BLOCK_SIZE + 1")
|
||||
self.move_tip(15)
|
||||
b24 = self.next_block(24, spend=out[6])
|
||||
script_length = MAX_BLOCK_SIZE - len(b24.serialize()) - 69
|
||||
script_length = MAX_BLOCK_SIZE - b24.get_weight() - 69
|
||||
script_output = CScript([b'\x00' * (script_length + 1)])
|
||||
tx.vout = [CTxOut(0, script_output)]
|
||||
b24 = self.update_block(24, [tx])
|
||||
assert_equal(len(b24.serialize()), MAX_BLOCK_SIZE + 1)
|
||||
assert_equal(b24.get_weight(), MAX_BLOCK_SIZE + 1)
|
||||
self.send_blocks([b24], success=False, reject_reason='bad-blk-length', reconnect=True)
|
||||
|
||||
b25 = self.next_block(25, spend=out[7])
|
||||
@ -500,13 +500,13 @@ class FullBlockTest(BitcoinTestFramework):
|
||||
# Until block is full, add tx's with 1 satoshi to p2sh_script, the rest to OP_TRUE
|
||||
tx_new = None
|
||||
tx_last = tx
|
||||
total_size = len(b39.serialize())
|
||||
while(total_size < MAX_BLOCK_SIZE):
|
||||
total_weight = b39.get_weight()
|
||||
while total_weight < MAX_BLOCK_SIZE:
|
||||
tx_new = self.create_tx(tx_last, 1, 1, p2sh_script)
|
||||
tx_new.vout.append(CTxOut(tx_last.vout[1].nValue - 1, CScript([OP_TRUE])))
|
||||
tx_new.rehash()
|
||||
total_size += len(tx_new.serialize())
|
||||
if total_size >= MAX_BLOCK_SIZE:
|
||||
total_weight += tx_new.get_weight()
|
||||
if total_weight >= MAX_BLOCK_SIZE:
|
||||
break
|
||||
b39.vtx.append(tx_new) # add tx to block
|
||||
tx_last = tx_new
|
||||
@ -517,7 +517,7 @@ class FullBlockTest(BitcoinTestFramework):
|
||||
# Make sure we didn't accidentally make too big a block. Note that the
|
||||
# size of the block has non-determinism due to the ECDSA signature in
|
||||
# the first transaction.
|
||||
while (len(b39.serialize()) >= MAX_BLOCK_SIZE):
|
||||
while b39.get_weight() >= MAX_BLOCK_SIZE:
|
||||
del b39.vtx[-1]
|
||||
|
||||
b39 = self.update_block(39, [])
|
||||
@ -938,7 +938,7 @@ class FullBlockTest(BitcoinTestFramework):
|
||||
tx.vout.append(CTxOut(0, script_output))
|
||||
tx.vin.append(CTxIn(COutPoint(b64a.vtx[1].sha256, 0)))
|
||||
b64a = self.update_block("64a", [tx])
|
||||
assert_equal(len(b64a.serialize()), MAX_BLOCK_SIZE + 8)
|
||||
assert_equal(b64a.get_weight(), MAX_BLOCK_SIZE + 8)
|
||||
self.send_blocks([b64a], success=False, reject_reason='non-canonical ReadCompactSize()')
|
||||
|
||||
# dashd doesn't disconnect us for sending a bloated block, but if we subsequently
|
||||
@ -952,7 +952,7 @@ class FullBlockTest(BitcoinTestFramework):
|
||||
b64 = CBlock(b64a)
|
||||
b64.vtx = copy.deepcopy(b64a.vtx)
|
||||
assert_equal(b64.hash, b64a.hash)
|
||||
assert_equal(len(b64.serialize()), MAX_BLOCK_SIZE)
|
||||
assert_equal(b64.get_weight(), MAX_BLOCK_SIZE)
|
||||
self.blocks[64] = b64
|
||||
b64 = self.update_block(64, [])
|
||||
self.send_blocks([b64], True)
|
||||
@ -1290,12 +1290,12 @@ class FullBlockTest(BitcoinTestFramework):
|
||||
for i in range(89, LARGE_REORG_SIZE + 89):
|
||||
b = self.next_block(i, spend)
|
||||
tx = CTransaction()
|
||||
script_length = MAX_BLOCK_SIZE - len(b.serialize()) - 69
|
||||
script_length = MAX_BLOCK_SIZE - b.get_weight() - 69
|
||||
script_output = CScript([b'\x00' * script_length])
|
||||
tx.vout.append(CTxOut(0, script_output))
|
||||
tx.vin.append(CTxIn(COutPoint(b.vtx[1].sha256, 0)))
|
||||
b = self.update_block(i, [tx])
|
||||
assert_equal(len(b.serialize()), MAX_BLOCK_SIZE)
|
||||
assert_equal(b.get_weight(), MAX_BLOCK_SIZE)
|
||||
blocks.append(b)
|
||||
self.save_spendable_output()
|
||||
spend = self.get_spendable_output()
|
||||
|
@ -55,8 +55,8 @@ class PrioritiseTransactionTest(BitcoinTestFramework):
|
||||
txids[i] = create_lots_of_big_transactions(self.nodes[0], self.txouts, utxos[start_range:end_range], end_range - start_range, (i+1)*base_fee)
|
||||
|
||||
# Make sure that the size of each group of transactions exceeds
|
||||
# MAX_BLOCK_SIZE -- otherwise the test needs to be revised to create
|
||||
# more transactions.
|
||||
# MAX_BLOCK_SIZE -- otherwise the test needs to be revised to
|
||||
# create more transactions.
|
||||
mempool = self.nodes[0].getrawmempool(True)
|
||||
sizes = [0, 0, 0]
|
||||
for i in range(3):
|
||||
|
@ -133,7 +133,7 @@ class P2PPermissionsTests(BitcoinTestFramework):
|
||||
tx.vout[0].nValue += 1
|
||||
txid = tx.rehash()
|
||||
# Send the transaction twice. The first time, it'll be rejected by ATMP because it conflicts
|
||||
# with a mempool transaction. The second time, it'll be in the recentRejects filter.
|
||||
# with a mempool transaction. The second time, it'll be in the m_recent_rejects filter.
|
||||
p2p_rebroadcast_wallet.send_txs_and_test(
|
||||
[tx],
|
||||
self.nodes[1],
|
||||
|
@ -546,6 +546,7 @@ class CTransaction:
|
||||
def get_vsize(self):
|
||||
return len(self.serialize())
|
||||
|
||||
# it's just a helper that return vsize to reduce conflicts during backporting
|
||||
def get_weight(self):
|
||||
return self.get_vsize()
|
||||
|
||||
@ -681,6 +682,10 @@ class CBlock(CBlockHeader):
|
||||
self.nNonce += 1
|
||||
self.rehash()
|
||||
|
||||
# it's just a helper that return vsize to reduce conflicts during backporting
|
||||
def get_weight(self):
|
||||
return len(self.serialize())
|
||||
|
||||
def __repr__(self):
|
||||
return "CBlock(nVersion=%i hashPrevBlock=%064x hashMerkleRoot=%064x nTime=%s nBits=%08x nNonce=%08x vtx=%s)" \
|
||||
% (self.nVersion, self.hashPrevBlock, self.hashMerkleRoot,
|
||||
|
@ -72,11 +72,39 @@ class ListDescriptorsTest(BitcoinTestFramework):
|
||||
],
|
||||
}
|
||||
assert_equal(expected, wallet.listdescriptors())
|
||||
assert_equal(expected, wallet.listdescriptors(False))
|
||||
|
||||
self.log.info('Test list private descriptors')
|
||||
expected_private = {
|
||||
'wallet_name': 'w2',
|
||||
'descriptors': [
|
||||
{'desc': descsum_create('pkh(' + xprv + hardened_path + '/0/*)'),
|
||||
'timestamp': 1296688602,
|
||||
'active': False,
|
||||
'range': [0, 0],
|
||||
'next': 0},
|
||||
],
|
||||
}
|
||||
assert_equal(expected_private, wallet.listdescriptors(True))
|
||||
|
||||
self.log.info("Test listdescriptors with encrypted wallet")
|
||||
wallet.encryptwallet("pass")
|
||||
assert_equal(expected, wallet.listdescriptors())
|
||||
|
||||
self.log.info('Test list private descriptors with encrypted wallet')
|
||||
assert_raises_rpc_error(-13, 'Please enter the wallet passphrase with walletpassphrase first.', wallet.listdescriptors, True)
|
||||
wallet.walletpassphrase(passphrase="pass", timeout=1000000)
|
||||
assert_equal(expected_private, wallet.listdescriptors(True))
|
||||
|
||||
self.log.info('Test list private descriptors with watch-only wallet')
|
||||
node.createwallet(wallet_name='watch-only', descriptors=True, disable_private_keys=True)
|
||||
watch_only_wallet = node.get_wallet_rpc('watch-only')
|
||||
watch_only_wallet.importdescriptors([{
|
||||
'desc': descsum_create('pkh(' + xpub_acc + ')'),
|
||||
'timestamp': 1296688602,
|
||||
}])
|
||||
assert_raises_rpc_error(-4, 'Can\'t get descriptor string', watch_only_wallet.listdescriptors, True)
|
||||
|
||||
self.log.info('Test non-active non-range combo descriptor')
|
||||
node.createwallet(wallet_name='w4', blank=True, descriptors=True)
|
||||
wallet = node.get_wallet_rpc('w4')
|
||||
|
Loading…
Reference in New Issue
Block a user