Initial Vexta Core v0.1.0 release
This commit is contained in:
commit
25c454329d
2481 changed files with 689289 additions and 0 deletions
848
src/wallet/bdb.cpp
Normal file
848
src/wallet/bdb.cpp
Normal file
|
|
@ -0,0 +1,848 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <wallet/bdb.h>
|
||||
#include <wallet/db.h>
|
||||
|
||||
#include <util/strencodings.h>
|
||||
#include <util/translation.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef WIN32
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
//! Make sure database has a unique fileid within the environment. If it
|
||||
//! doesn't, throw an error. BDB caches do not work properly when more than one
|
||||
//! open database has the same fileid (values written to one database may show
|
||||
//! up in reads to other databases).
|
||||
//!
|
||||
//! BerkeleyDB generates unique fileids by default
|
||||
//! (https://docs.oracle.com/cd/E17275_01/html/programmer_reference/program_copy.html),
|
||||
//! so Vexta should never create different databases with the same fileid, but
|
||||
//! this error can be triggered if users manually copy database files.
|
||||
void CheckUniqueFileid(const BerkeleyEnvironment& env, const std::string& filename, Db& db, WalletDatabaseFileId& fileid)
|
||||
{
|
||||
if (env.IsMock()) return;
|
||||
|
||||
int ret = db.get_mpf()->get_fileid(fileid.value);
|
||||
if (ret != 0) {
|
||||
throw std::runtime_error(strprintf("BerkeleyDatabase: Can't open database %s (get_fileid failed with %d)", filename, ret));
|
||||
}
|
||||
|
||||
for (const auto& item : env.m_fileids) {
|
||||
if (fileid == item.second && &fileid != &item.second) {
|
||||
throw std::runtime_error(strprintf("BerkeleyDatabase: Can't open database %s (duplicates fileid %s from %s)", filename,
|
||||
HexStr(item.second.value), item.first));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RecursiveMutex cs_db;
|
||||
std::map<std::string, std::weak_ptr<BerkeleyEnvironment>> g_dbenvs GUARDED_BY(cs_db); //!< Map from directory name to db environment.
|
||||
} // namespace
|
||||
|
||||
bool WalletDatabaseFileId::operator==(const WalletDatabaseFileId& rhs) const
|
||||
{
|
||||
return memcmp(value, &rhs.value, sizeof(value)) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param[in] env_directory Path to environment directory
|
||||
* @return A shared pointer to the BerkeleyEnvironment object for the wallet directory, never empty because ~BerkeleyEnvironment
|
||||
* erases the weak pointer from the g_dbenvs map.
|
||||
* @post A new BerkeleyEnvironment weak pointer is inserted into g_dbenvs if the directory path key was not already in the map.
|
||||
*/
|
||||
std::shared_ptr<BerkeleyEnvironment> GetBerkeleyEnv(const fs::path& env_directory)
|
||||
{
|
||||
LOCK(cs_db);
|
||||
auto inserted = g_dbenvs.emplace(env_directory.string(), std::weak_ptr<BerkeleyEnvironment>());
|
||||
if (inserted.second) {
|
||||
auto env = std::make_shared<BerkeleyEnvironment>(env_directory.string());
|
||||
inserted.first->second = env;
|
||||
return env;
|
||||
}
|
||||
return inserted.first->second.lock();
|
||||
}
|
||||
|
||||
//
|
||||
// BerkeleyBatch
|
||||
//
|
||||
|
||||
void BerkeleyEnvironment::Close()
|
||||
{
|
||||
if (!fDbEnvInit)
|
||||
return;
|
||||
|
||||
fDbEnvInit = false;
|
||||
|
||||
for (auto& db : m_databases) {
|
||||
BerkeleyDatabase& database = db.second.get();
|
||||
assert(database.m_refcount <= 0);
|
||||
if (database.m_db) {
|
||||
database.m_db->close(0);
|
||||
database.m_db.reset();
|
||||
}
|
||||
}
|
||||
|
||||
FILE* error_file = nullptr;
|
||||
dbenv->get_errfile(&error_file);
|
||||
|
||||
int ret = dbenv->close(0);
|
||||
if (ret != 0)
|
||||
LogPrintf("BerkeleyEnvironment::Close: Error %d closing database environment: %s\n", ret, DbEnv::strerror(ret));
|
||||
if (!fMockDb)
|
||||
DbEnv((u_int32_t)0).remove(strPath.c_str(), 0);
|
||||
|
||||
if (error_file) fclose(error_file);
|
||||
|
||||
UnlockDirectory(strPath, ".walletlock");
|
||||
}
|
||||
|
||||
void BerkeleyEnvironment::Reset()
|
||||
{
|
||||
dbenv.reset(new DbEnv(DB_CXX_NO_EXCEPTIONS));
|
||||
fDbEnvInit = false;
|
||||
fMockDb = false;
|
||||
}
|
||||
|
||||
BerkeleyEnvironment::BerkeleyEnvironment(const fs::path& dir_path) : strPath(dir_path.string())
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
|
||||
BerkeleyEnvironment::~BerkeleyEnvironment()
|
||||
{
|
||||
LOCK(cs_db);
|
||||
g_dbenvs.erase(strPath);
|
||||
Close();
|
||||
}
|
||||
|
||||
bool BerkeleyEnvironment::Open(bilingual_str& err)
|
||||
{
|
||||
if (fDbEnvInit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
fs::path pathIn = strPath;
|
||||
TryCreateDirectories(pathIn);
|
||||
if (!LockDirectory(pathIn, ".walletlock")) {
|
||||
LogPrintf("Cannot obtain a lock on wallet directory %s. Another instance of Vexta may be using it.\n", strPath);
|
||||
err = strprintf(_("Error initializing wallet database environment %s!"), Directory());
|
||||
return false;
|
||||
}
|
||||
|
||||
fs::path pathLogDir = pathIn / "database";
|
||||
TryCreateDirectories(pathLogDir);
|
||||
fs::path pathErrorFile = pathIn / "db.log";
|
||||
LogPrintf("BerkeleyEnvironment::Open: LogDir=%s ErrorFile=%s\n", pathLogDir.string(), pathErrorFile.string());
|
||||
|
||||
unsigned int nEnvFlags = 0;
|
||||
if (gArgs.GetBoolArg("-privdb", DEFAULT_WALLET_PRIVDB))
|
||||
nEnvFlags |= DB_PRIVATE;
|
||||
|
||||
dbenv->set_lg_dir(pathLogDir.string().c_str());
|
||||
dbenv->set_cachesize(0, 0x100000, 1); // 1 MiB should be enough for just the wallet
|
||||
dbenv->set_lg_bsize(0x10000);
|
||||
dbenv->set_lg_max(1048576);
|
||||
dbenv->set_lk_max_locks(40000);
|
||||
dbenv->set_lk_max_objects(40000);
|
||||
dbenv->set_errfile(fsbridge::fopen(pathErrorFile, "a")); /// debug
|
||||
dbenv->set_flags(DB_AUTO_COMMIT, 1);
|
||||
dbenv->set_flags(DB_TXN_WRITE_NOSYNC, 1);
|
||||
dbenv->log_set_config(DB_LOG_AUTO_REMOVE, 1);
|
||||
int ret = dbenv->open(strPath.c_str(),
|
||||
DB_CREATE |
|
||||
DB_INIT_LOCK |
|
||||
DB_INIT_LOG |
|
||||
DB_INIT_MPOOL |
|
||||
DB_INIT_TXN |
|
||||
DB_THREAD |
|
||||
DB_RECOVER |
|
||||
nEnvFlags,
|
||||
S_IRUSR | S_IWUSR);
|
||||
if (ret != 0) {
|
||||
LogPrintf("BerkeleyEnvironment::Open: Error %d opening database environment: %s\n", ret, DbEnv::strerror(ret));
|
||||
int ret2 = dbenv->close(0);
|
||||
if (ret2 != 0) {
|
||||
LogPrintf("BerkeleyEnvironment::Open: Error %d closing failed database environment: %s\n", ret2, DbEnv::strerror(ret2));
|
||||
}
|
||||
Reset();
|
||||
err = strprintf(_("Error initializing wallet database environment %s!"), Directory());
|
||||
if (ret == DB_RUNRECOVERY) {
|
||||
err += Untranslated(" ") + _("This error could occur if this wallet was not shutdown cleanly and was last loaded using a build with a newer version of Berkeley DB. If so, please use the software that last loaded this wallet");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fDbEnvInit = true;
|
||||
fMockDb = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
//! Construct an in-memory mock Berkeley environment for testing
|
||||
BerkeleyEnvironment::BerkeleyEnvironment()
|
||||
{
|
||||
Reset();
|
||||
|
||||
LogPrint(BCLog::WALLETDB, "BerkeleyEnvironment::MakeMock\n");
|
||||
|
||||
dbenv->set_cachesize(1, 0, 1);
|
||||
dbenv->set_lg_bsize(10485760 * 4);
|
||||
dbenv->set_lg_max(10485760);
|
||||
dbenv->set_lk_max_locks(10000);
|
||||
dbenv->set_lk_max_objects(10000);
|
||||
dbenv->set_flags(DB_AUTO_COMMIT, 1);
|
||||
dbenv->log_set_config(DB_LOG_IN_MEMORY, 1);
|
||||
int ret = dbenv->open(nullptr,
|
||||
DB_CREATE |
|
||||
DB_INIT_LOCK |
|
||||
DB_INIT_LOG |
|
||||
DB_INIT_MPOOL |
|
||||
DB_INIT_TXN |
|
||||
DB_THREAD |
|
||||
DB_PRIVATE,
|
||||
S_IRUSR | S_IWUSR);
|
||||
if (ret > 0) {
|
||||
throw std::runtime_error(strprintf("BerkeleyEnvironment::MakeMock: Error %d opening database environment.", ret));
|
||||
}
|
||||
|
||||
fDbEnvInit = true;
|
||||
fMockDb = true;
|
||||
}
|
||||
|
||||
BerkeleyBatch::SafeDbt::SafeDbt()
|
||||
{
|
||||
m_dbt.set_flags(DB_DBT_MALLOC);
|
||||
}
|
||||
|
||||
BerkeleyBatch::SafeDbt::SafeDbt(void* data, size_t size)
|
||||
: m_dbt(data, size)
|
||||
{
|
||||
}
|
||||
|
||||
BerkeleyBatch::SafeDbt::~SafeDbt()
|
||||
{
|
||||
if (m_dbt.get_data() != nullptr) {
|
||||
// Clear memory, e.g. in case it was a private key
|
||||
memory_cleanse(m_dbt.get_data(), m_dbt.get_size());
|
||||
// under DB_DBT_MALLOC, data is malloced by the Dbt, but must be
|
||||
// freed by the caller.
|
||||
// https://docs.oracle.com/cd/E17275_01/html/api_reference/C/dbt.html
|
||||
if (m_dbt.get_flags() & DB_DBT_MALLOC) {
|
||||
free(m_dbt.get_data());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const void* BerkeleyBatch::SafeDbt::get_data() const
|
||||
{
|
||||
return m_dbt.get_data();
|
||||
}
|
||||
|
||||
u_int32_t BerkeleyBatch::SafeDbt::get_size() const
|
||||
{
|
||||
return m_dbt.get_size();
|
||||
}
|
||||
|
||||
BerkeleyBatch::SafeDbt::operator Dbt*()
|
||||
{
|
||||
return &m_dbt;
|
||||
}
|
||||
|
||||
bool BerkeleyDatabase::Verify(bilingual_str& errorStr)
|
||||
{
|
||||
fs::path walletDir = env->Directory();
|
||||
fs::path file_path = walletDir / strFile;
|
||||
|
||||
LogPrintf("Using BerkeleyDB version %s\n", BerkeleyDatabaseVersion());
|
||||
LogPrintf("Using wallet %s\n", file_path.string());
|
||||
|
||||
if (!env->Open(errorStr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fs::exists(file_path))
|
||||
{
|
||||
assert(m_refcount == 0);
|
||||
|
||||
Db db(env->dbenv.get(), 0);
|
||||
int result = db.verify(strFile.c_str(), nullptr, nullptr, 0);
|
||||
if (result != 0) {
|
||||
errorStr = strprintf(_("%s corrupt. Try using the wallet tool digibyte-wallet to salvage or restoring a backup."), file_path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// also return true if files does not exists
|
||||
return true;
|
||||
}
|
||||
|
||||
void BerkeleyEnvironment::CheckpointLSN(const std::string& strFile)
|
||||
{
|
||||
dbenv->txn_checkpoint(0, 0, 0);
|
||||
if (fMockDb)
|
||||
return;
|
||||
dbenv->lsn_reset(strFile.c_str(), 0);
|
||||
}
|
||||
|
||||
BerkeleyDatabase::~BerkeleyDatabase()
|
||||
{
|
||||
if (env) {
|
||||
LOCK(cs_db);
|
||||
env->CloseDb(strFile);
|
||||
assert(!m_db);
|
||||
size_t erased = env->m_databases.erase(strFile);
|
||||
assert(erased == 1);
|
||||
env->m_fileids.erase(strFile);
|
||||
}
|
||||
}
|
||||
|
||||
BerkeleyBatch::BerkeleyBatch(BerkeleyDatabase& database, const bool read_only, bool fFlushOnCloseIn) : pdb(nullptr), activeTxn(nullptr), m_cursor(nullptr), m_database(database)
|
||||
{
|
||||
database.AddRef();
|
||||
database.Open();
|
||||
fReadOnly = read_only;
|
||||
fFlushOnClose = fFlushOnCloseIn;
|
||||
env = database.env.get();
|
||||
pdb = database.m_db.get();
|
||||
strFile = database.strFile;
|
||||
if (!Exists(std::string("version"))) {
|
||||
bool fTmp = fReadOnly;
|
||||
fReadOnly = false;
|
||||
Write(std::string("version"), CLIENT_VERSION);
|
||||
fReadOnly = fTmp;
|
||||
}
|
||||
}
|
||||
|
||||
void BerkeleyDatabase::Open()
|
||||
{
|
||||
unsigned int nFlags = DB_THREAD | DB_CREATE;
|
||||
|
||||
{
|
||||
LOCK(cs_db);
|
||||
bilingual_str open_err;
|
||||
if (!env->Open(open_err))
|
||||
throw std::runtime_error("BerkeleyDatabase: Failed to open database environment.");
|
||||
|
||||
if (m_db == nullptr) {
|
||||
int ret;
|
||||
std::unique_ptr<Db> pdb_temp = std::make_unique<Db>(env->dbenv.get(), 0);
|
||||
|
||||
bool fMockDb = env->IsMock();
|
||||
if (fMockDb) {
|
||||
DbMpoolFile* mpf = pdb_temp->get_mpf();
|
||||
ret = mpf->set_flags(DB_MPOOL_NOFILE, 1);
|
||||
if (ret != 0) {
|
||||
throw std::runtime_error(strprintf("BerkeleyDatabase: Failed to configure for no temp file backing for database %s", strFile));
|
||||
}
|
||||
}
|
||||
|
||||
ret = pdb_temp->open(nullptr, // Txn pointer
|
||||
fMockDb ? nullptr : strFile.c_str(), // Filename
|
||||
fMockDb ? strFile.c_str() : "main", // Logical db name
|
||||
DB_BTREE, // Database type
|
||||
nFlags, // Flags
|
||||
0);
|
||||
|
||||
if (ret != 0) {
|
||||
throw std::runtime_error(strprintf("BerkeleyDatabase: Error %d, can't open database %s", ret, strFile));
|
||||
}
|
||||
|
||||
// Call CheckUniqueFileid on the containing BDB environment to
|
||||
// avoid BDB data consistency bugs that happen when different data
|
||||
// files in the same environment have the same fileid.
|
||||
CheckUniqueFileid(*env, strFile, *pdb_temp, this->env->m_fileids[strFile]);
|
||||
|
||||
m_db.reset(pdb_temp.release());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BerkeleyBatch::Flush()
|
||||
{
|
||||
if (activeTxn)
|
||||
return;
|
||||
|
||||
// Flush database activity from memory pool to disk log
|
||||
unsigned int nMinutes = 0;
|
||||
if (fReadOnly)
|
||||
nMinutes = 1;
|
||||
|
||||
if (env) { // env is nullptr for dummy databases (i.e. in tests). Don't actually flush if env is nullptr so we don't segfault
|
||||
env->dbenv->txn_checkpoint(nMinutes ? gArgs.GetArg("-dblogsize", DEFAULT_WALLET_DBLOGSIZE) * 1024 : 0, nMinutes, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void BerkeleyDatabase::IncrementUpdateCounter()
|
||||
{
|
||||
++nUpdateCounter;
|
||||
}
|
||||
|
||||
BerkeleyBatch::~BerkeleyBatch()
|
||||
{
|
||||
Close();
|
||||
m_database.RemoveRef();
|
||||
}
|
||||
|
||||
void BerkeleyBatch::Close()
|
||||
{
|
||||
if (!pdb)
|
||||
return;
|
||||
if (activeTxn)
|
||||
activeTxn->abort();
|
||||
activeTxn = nullptr;
|
||||
pdb = nullptr;
|
||||
CloseCursor();
|
||||
|
||||
if (fFlushOnClose)
|
||||
Flush();
|
||||
}
|
||||
|
||||
void BerkeleyEnvironment::CloseDb(const std::string& strFile)
|
||||
{
|
||||
{
|
||||
LOCK(cs_db);
|
||||
auto it = m_databases.find(strFile);
|
||||
assert(it != m_databases.end());
|
||||
BerkeleyDatabase& database = it->second.get();
|
||||
if (database.m_db) {
|
||||
// Close the database handle
|
||||
database.m_db->close(0);
|
||||
database.m_db.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BerkeleyEnvironment::ReloadDbEnv()
|
||||
{
|
||||
// Make sure that no Db's are in use
|
||||
AssertLockNotHeld(cs_db);
|
||||
std::unique_lock<RecursiveMutex> lock(cs_db);
|
||||
m_db_in_use.wait(lock, [this](){
|
||||
for (auto& db : m_databases) {
|
||||
if (db.second.get().m_refcount > 0) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
std::vector<std::string> filenames;
|
||||
for (auto it : m_databases) {
|
||||
filenames.push_back(it.first);
|
||||
}
|
||||
// Close the individual Db's
|
||||
for (const std::string& filename : filenames) {
|
||||
CloseDb(filename);
|
||||
}
|
||||
// Reset the environment
|
||||
Flush(true); // This will flush and close the environment
|
||||
Reset();
|
||||
bilingual_str open_err;
|
||||
Open(open_err);
|
||||
}
|
||||
|
||||
bool BerkeleyDatabase::Rewrite(const char* pszSkip)
|
||||
{
|
||||
while (true) {
|
||||
{
|
||||
LOCK(cs_db);
|
||||
if (m_refcount <= 0) {
|
||||
// Flush log data to the dat file
|
||||
env->CloseDb(strFile);
|
||||
env->CheckpointLSN(strFile);
|
||||
m_refcount = -1;
|
||||
|
||||
bool fSuccess = true;
|
||||
LogPrintf("BerkeleyBatch::Rewrite: Rewriting %s...\n", strFile);
|
||||
std::string strFileRes = strFile + ".rewrite";
|
||||
{ // surround usage of db with extra {}
|
||||
BerkeleyBatch db(*this, true);
|
||||
std::unique_ptr<Db> pdbCopy = std::make_unique<Db>(env->dbenv.get(), 0);
|
||||
|
||||
int ret = pdbCopy->open(nullptr, // Txn pointer
|
||||
strFileRes.c_str(), // Filename
|
||||
"main", // Logical db name
|
||||
DB_BTREE, // Database type
|
||||
DB_CREATE, // Flags
|
||||
0);
|
||||
if (ret > 0) {
|
||||
LogPrintf("BerkeleyBatch::Rewrite: Can't create database file %s\n", strFileRes);
|
||||
fSuccess = false;
|
||||
}
|
||||
|
||||
if (db.StartCursor()) {
|
||||
while (fSuccess) {
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
|
||||
bool complete;
|
||||
bool ret1 = db.ReadAtCursor(ssKey, ssValue, complete);
|
||||
if (complete) {
|
||||
break;
|
||||
} else if (!ret1) {
|
||||
fSuccess = false;
|
||||
break;
|
||||
}
|
||||
if (pszSkip &&
|
||||
strncmp((const char*)ssKey.data(), pszSkip, std::min(ssKey.size(), strlen(pszSkip))) == 0)
|
||||
continue;
|
||||
if (strncmp((const char*)ssKey.data(), "\x07version", 8) == 0) {
|
||||
// Update version:
|
||||
ssValue.clear();
|
||||
ssValue << CLIENT_VERSION;
|
||||
}
|
||||
Dbt datKey(ssKey.data(), ssKey.size());
|
||||
Dbt datValue(ssValue.data(), ssValue.size());
|
||||
int ret2 = pdbCopy->put(nullptr, &datKey, &datValue, DB_NOOVERWRITE);
|
||||
if (ret2 > 0)
|
||||
fSuccess = false;
|
||||
}
|
||||
db.CloseCursor();
|
||||
}
|
||||
if (fSuccess) {
|
||||
db.Close();
|
||||
env->CloseDb(strFile);
|
||||
if (pdbCopy->close(0))
|
||||
fSuccess = false;
|
||||
} else {
|
||||
pdbCopy->close(0);
|
||||
}
|
||||
}
|
||||
if (fSuccess) {
|
||||
Db dbA(env->dbenv.get(), 0);
|
||||
if (dbA.remove(strFile.c_str(), nullptr, 0))
|
||||
fSuccess = false;
|
||||
Db dbB(env->dbenv.get(), 0);
|
||||
if (dbB.rename(strFileRes.c_str(), nullptr, strFile.c_str(), 0))
|
||||
fSuccess = false;
|
||||
}
|
||||
if (!fSuccess)
|
||||
LogPrintf("BerkeleyBatch::Rewrite: Failed to rewrite database file %s\n", strFileRes);
|
||||
return fSuccess;
|
||||
}
|
||||
}
|
||||
UninterruptibleSleep(std::chrono::milliseconds{100});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void BerkeleyEnvironment::Flush(bool fShutdown)
|
||||
{
|
||||
int64_t nStart = GetTimeMillis();
|
||||
// Flush log data to the actual data file on all files that are not in use
|
||||
LogPrint(BCLog::WALLETDB, "BerkeleyEnvironment::Flush: [%s] Flush(%s)%s\n", strPath, fShutdown ? "true" : "false", fDbEnvInit ? "" : " database not started");
|
||||
if (!fDbEnvInit)
|
||||
return;
|
||||
{
|
||||
LOCK(cs_db);
|
||||
bool no_dbs_accessed = true;
|
||||
for (auto& db_it : m_databases) {
|
||||
std::string strFile = db_it.first;
|
||||
int nRefCount = db_it.second.get().m_refcount;
|
||||
if (nRefCount < 0) continue;
|
||||
LogPrint(BCLog::WALLETDB, "BerkeleyEnvironment::Flush: Flushing %s (refcount = %d)...\n", strFile, nRefCount);
|
||||
if (nRefCount == 0) {
|
||||
// Move log data to the dat file
|
||||
CloseDb(strFile);
|
||||
LogPrint(BCLog::WALLETDB, "BerkeleyEnvironment::Flush: %s checkpoint\n", strFile);
|
||||
dbenv->txn_checkpoint(0, 0, 0);
|
||||
LogPrint(BCLog::WALLETDB, "BerkeleyEnvironment::Flush: %s detach\n", strFile);
|
||||
if (!fMockDb)
|
||||
dbenv->lsn_reset(strFile.c_str(), 0);
|
||||
LogPrint(BCLog::WALLETDB, "BerkeleyEnvironment::Flush: %s closed\n", strFile);
|
||||
nRefCount = -1;
|
||||
} else {
|
||||
no_dbs_accessed = false;
|
||||
}
|
||||
}
|
||||
LogPrint(BCLog::WALLETDB, "BerkeleyEnvironment::Flush: Flush(%s)%s took %15dms\n", fShutdown ? "true" : "false", fDbEnvInit ? "" : " database not started", GetTimeMillis() - nStart);
|
||||
if (fShutdown) {
|
||||
char** listp;
|
||||
if (no_dbs_accessed) {
|
||||
dbenv->log_archive(&listp, DB_ARCH_REMOVE);
|
||||
Close();
|
||||
if (!fMockDb) {
|
||||
fs::remove_all(fs::path(strPath) / "database");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool BerkeleyDatabase::PeriodicFlush()
|
||||
{
|
||||
// Don't flush if we can't acquire the lock.
|
||||
TRY_LOCK(cs_db, lockDb);
|
||||
if (!lockDb) return false;
|
||||
|
||||
// Don't flush if any databases are in use
|
||||
for (auto& it : env->m_databases) {
|
||||
if (it.second.get().m_refcount > 0) return false;
|
||||
}
|
||||
|
||||
// Don't flush if there haven't been any batch writes for this database.
|
||||
if (m_refcount < 0) return false;
|
||||
|
||||
LogPrint(BCLog::WALLETDB, "Flushing %s\n", strFile);
|
||||
int64_t nStart = GetTimeMillis();
|
||||
|
||||
// Flush wallet file so it's self contained
|
||||
env->CloseDb(strFile);
|
||||
env->CheckpointLSN(strFile);
|
||||
m_refcount = -1;
|
||||
|
||||
LogPrint(BCLog::WALLETDB, "Flushed %s %dms\n", strFile, GetTimeMillis() - nStart);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BerkeleyDatabase::Backup(const std::string& strDest) const
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
{
|
||||
LOCK(cs_db);
|
||||
if (m_refcount <= 0)
|
||||
{
|
||||
// Flush log data to the dat file
|
||||
env->CloseDb(strFile);
|
||||
env->CheckpointLSN(strFile);
|
||||
|
||||
// Copy wallet file
|
||||
fs::path pathSrc = env->Directory() / strFile;
|
||||
fs::path pathDest(strDest);
|
||||
if (fs::is_directory(pathDest))
|
||||
pathDest /= strFile;
|
||||
|
||||
try {
|
||||
if (fs::equivalent(pathSrc, pathDest)) {
|
||||
LogPrintf("cannot backup to wallet source file %s\n", pathDest.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
fs::copy_file(pathSrc, pathDest, fs::copy_options::overwrite_existing);
|
||||
LogPrintf("copied %s to %s\n", strFile, pathDest.string());
|
||||
return true;
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
LogPrintf("error copying %s to %s - %s\n", strFile, pathDest.string(), fsbridge::get_filesystem_error_message(e));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
UninterruptibleSleep(std::chrono::milliseconds{100});
|
||||
}
|
||||
}
|
||||
|
||||
void BerkeleyDatabase::Flush()
|
||||
{
|
||||
env->Flush(false);
|
||||
}
|
||||
|
||||
void BerkeleyDatabase::Close()
|
||||
{
|
||||
env->Flush(true);
|
||||
}
|
||||
|
||||
void BerkeleyDatabase::ReloadDbEnv()
|
||||
{
|
||||
env->ReloadDbEnv();
|
||||
}
|
||||
|
||||
bool BerkeleyBatch::StartCursor()
|
||||
{
|
||||
assert(!m_cursor);
|
||||
if (!pdb)
|
||||
return false;
|
||||
int ret = pdb->cursor(nullptr, &m_cursor, 0);
|
||||
return ret == 0;
|
||||
}
|
||||
|
||||
bool BerkeleyBatch::ReadAtCursor(CDataStream& ssKey, CDataStream& ssValue, bool& complete)
|
||||
{
|
||||
complete = false;
|
||||
if (m_cursor == nullptr) return false;
|
||||
// Read at cursor
|
||||
SafeDbt datKey;
|
||||
SafeDbt datValue;
|
||||
int ret = m_cursor->get(datKey, datValue, DB_NEXT);
|
||||
if (ret == DB_NOTFOUND) {
|
||||
complete = true;
|
||||
}
|
||||
if (ret != 0)
|
||||
return false;
|
||||
else if (datKey.get_data() == nullptr || datValue.get_data() == nullptr)
|
||||
return false;
|
||||
|
||||
// Convert to streams
|
||||
ssKey.SetType(SER_DISK);
|
||||
ssKey.clear();
|
||||
ssKey.write((char*)datKey.get_data(), datKey.get_size());
|
||||
ssValue.SetType(SER_DISK);
|
||||
ssValue.clear();
|
||||
ssValue.write((char*)datValue.get_data(), datValue.get_size());
|
||||
return true;
|
||||
}
|
||||
|
||||
void BerkeleyBatch::CloseCursor()
|
||||
{
|
||||
if (!m_cursor) return;
|
||||
m_cursor->close();
|
||||
m_cursor = nullptr;
|
||||
}
|
||||
|
||||
bool BerkeleyBatch::TxnBegin()
|
||||
{
|
||||
if (!pdb || activeTxn)
|
||||
return false;
|
||||
DbTxn* ptxn = env->TxnBegin();
|
||||
if (!ptxn)
|
||||
return false;
|
||||
activeTxn = ptxn;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BerkeleyBatch::TxnCommit()
|
||||
{
|
||||
if (!pdb || !activeTxn)
|
||||
return false;
|
||||
int ret = activeTxn->commit(0);
|
||||
activeTxn = nullptr;
|
||||
return (ret == 0);
|
||||
}
|
||||
|
||||
bool BerkeleyBatch::TxnAbort()
|
||||
{
|
||||
if (!pdb || !activeTxn)
|
||||
return false;
|
||||
int ret = activeTxn->abort();
|
||||
activeTxn = nullptr;
|
||||
return (ret == 0);
|
||||
}
|
||||
|
||||
bool BerkeleyDatabaseSanityCheck()
|
||||
{
|
||||
int major, minor;
|
||||
DbEnv::version(&major, &minor, nullptr);
|
||||
|
||||
/* If the major version differs, or the minor version of library is *older*
|
||||
* than the header that was compiled against, flag an error.
|
||||
*/
|
||||
if (major != DB_VERSION_MAJOR || minor < DB_VERSION_MINOR) {
|
||||
LogPrintf("BerkeleyDB database version conflict: header version is %d.%d, library version is %d.%d\n",
|
||||
DB_VERSION_MAJOR, DB_VERSION_MINOR, major, minor);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string BerkeleyDatabaseVersion()
|
||||
{
|
||||
return DbEnv::version(nullptr, nullptr, nullptr);
|
||||
}
|
||||
|
||||
bool BerkeleyBatch::ReadKey(CDataStream&& key, CDataStream& value)
|
||||
{
|
||||
if (!pdb)
|
||||
return false;
|
||||
|
||||
SafeDbt datKey(key.data(), key.size());
|
||||
|
||||
SafeDbt datValue;
|
||||
int ret = pdb->get(activeTxn, datKey, datValue, 0);
|
||||
if (ret == 0 && datValue.get_data() != nullptr) {
|
||||
value.write((char*)datValue.get_data(), datValue.get_size());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BerkeleyBatch::WriteKey(CDataStream&& key, CDataStream&& value, bool overwrite)
|
||||
{
|
||||
if (!pdb)
|
||||
return false;
|
||||
if (fReadOnly)
|
||||
assert(!"Write called on database in read-only mode");
|
||||
|
||||
SafeDbt datKey(key.data(), key.size());
|
||||
|
||||
SafeDbt datValue(value.data(), value.size());
|
||||
|
||||
int ret = pdb->put(activeTxn, datKey, datValue, (overwrite ? 0 : DB_NOOVERWRITE));
|
||||
return (ret == 0);
|
||||
}
|
||||
|
||||
bool BerkeleyBatch::EraseKey(CDataStream&& key)
|
||||
{
|
||||
if (!pdb)
|
||||
return false;
|
||||
if (fReadOnly)
|
||||
assert(!"Erase called on database in read-only mode");
|
||||
|
||||
SafeDbt datKey(key.data(), key.size());
|
||||
|
||||
int ret = pdb->del(activeTxn, datKey, 0);
|
||||
return (ret == 0 || ret == DB_NOTFOUND);
|
||||
}
|
||||
|
||||
bool BerkeleyBatch::HasKey(CDataStream&& key)
|
||||
{
|
||||
if (!pdb)
|
||||
return false;
|
||||
|
||||
SafeDbt datKey(key.data(), key.size());
|
||||
|
||||
int ret = pdb->exists(activeTxn, datKey, 0);
|
||||
return ret == 0;
|
||||
}
|
||||
|
||||
void BerkeleyDatabase::AddRef()
|
||||
{
|
||||
LOCK(cs_db);
|
||||
if (m_refcount < 0) {
|
||||
m_refcount = 1;
|
||||
} else {
|
||||
m_refcount++;
|
||||
}
|
||||
}
|
||||
|
||||
void BerkeleyDatabase::RemoveRef()
|
||||
{
|
||||
LOCK(cs_db);
|
||||
m_refcount--;
|
||||
if (env) env->m_db_in_use.notify_all();
|
||||
}
|
||||
|
||||
std::unique_ptr<DatabaseBatch> BerkeleyDatabase::MakeBatch(bool flush_on_close)
|
||||
{
|
||||
return std::make_unique<BerkeleyBatch>(*this, false, flush_on_close);
|
||||
}
|
||||
|
||||
std::unique_ptr<BerkeleyDatabase> MakeBerkeleyDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error)
|
||||
{
|
||||
fs::path data_file = BDBDataFile(path);
|
||||
std::unique_ptr<BerkeleyDatabase> db;
|
||||
{
|
||||
LOCK(cs_db); // Lock env.m_databases until insert in BerkeleyDatabase constructor
|
||||
std::string data_filename = data_file.filename().string();
|
||||
std::shared_ptr<BerkeleyEnvironment> env = GetBerkeleyEnv(data_file.parent_path());
|
||||
if (env->m_databases.count(data_filename)) {
|
||||
error = Untranslated(strprintf("Refusing to load database. Data file '%s' is already loaded.", (env->Directory() / data_filename).string()));
|
||||
status = DatabaseStatus::FAILED_ALREADY_LOADED;
|
||||
return nullptr;
|
||||
}
|
||||
db = std::make_unique<BerkeleyDatabase>(std::move(env), std::move(data_filename));
|
||||
}
|
||||
|
||||
if (options.verify && !db->Verify(error)) {
|
||||
status = DatabaseStatus::FAILED_VERIFY;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
status = DatabaseStatus::SUCCESS;
|
||||
return db;
|
||||
}
|
||||
233
src/wallet/bdb.h
Normal file
233
src/wallet/bdb.h
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_BDB_H
|
||||
#define DIGIBYTE_WALLET_BDB_H
|
||||
|
||||
#include <clientversion.h>
|
||||
#include <fs.h>
|
||||
#include <serialize.h>
|
||||
#include <streams.h>
|
||||
#include <util/system.h>
|
||||
#include <wallet/db.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__GNUC__) && !defined(__clang__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wsuggest-override"
|
||||
#endif
|
||||
#include <db_cxx.h>
|
||||
#if defined(__GNUC__) && !defined(__clang__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
struct bilingual_str;
|
||||
|
||||
static const unsigned int DEFAULT_WALLET_DBLOGSIZE = 100;
|
||||
static const bool DEFAULT_WALLET_PRIVDB = true;
|
||||
|
||||
struct WalletDatabaseFileId {
|
||||
u_int8_t value[DB_FILE_ID_LEN];
|
||||
bool operator==(const WalletDatabaseFileId& rhs) const;
|
||||
};
|
||||
|
||||
class BerkeleyDatabase;
|
||||
|
||||
class BerkeleyEnvironment
|
||||
{
|
||||
private:
|
||||
bool fDbEnvInit;
|
||||
bool fMockDb;
|
||||
// Don't change into fs::path, as that can result in
|
||||
// shutdown problems/crashes caused by a static initialized internal pointer.
|
||||
std::string strPath;
|
||||
|
||||
public:
|
||||
std::unique_ptr<DbEnv> dbenv;
|
||||
std::map<std::string, std::reference_wrapper<BerkeleyDatabase>> m_databases;
|
||||
std::unordered_map<std::string, WalletDatabaseFileId> m_fileids;
|
||||
std::condition_variable_any m_db_in_use;
|
||||
|
||||
explicit BerkeleyEnvironment(const fs::path& env_directory);
|
||||
BerkeleyEnvironment();
|
||||
~BerkeleyEnvironment();
|
||||
void Reset();
|
||||
|
||||
bool IsMock() const { return fMockDb; }
|
||||
bool IsInitialized() const { return fDbEnvInit; }
|
||||
fs::path Directory() const { return strPath; }
|
||||
|
||||
bool Open(bilingual_str& error);
|
||||
void Close();
|
||||
void Flush(bool fShutdown);
|
||||
void CheckpointLSN(const std::string& strFile);
|
||||
|
||||
void CloseDb(const std::string& strFile);
|
||||
void ReloadDbEnv();
|
||||
|
||||
DbTxn* TxnBegin(int flags = DB_TXN_WRITE_NOSYNC)
|
||||
{
|
||||
DbTxn* ptxn = nullptr;
|
||||
int ret = dbenv->txn_begin(nullptr, &ptxn, flags);
|
||||
if (!ptxn || ret != 0)
|
||||
return nullptr;
|
||||
return ptxn;
|
||||
}
|
||||
};
|
||||
|
||||
/** Get BerkeleyEnvironment given a directory path. */
|
||||
std::shared_ptr<BerkeleyEnvironment> GetBerkeleyEnv(const fs::path& env_directory);
|
||||
|
||||
class BerkeleyBatch;
|
||||
|
||||
/** An instance of this class represents one database.
|
||||
* For BerkeleyDB this is just a (env, strFile) tuple.
|
||||
**/
|
||||
class BerkeleyDatabase : public WalletDatabase
|
||||
{
|
||||
public:
|
||||
BerkeleyDatabase() = delete;
|
||||
|
||||
/** Create DB handle to real database */
|
||||
BerkeleyDatabase(std::shared_ptr<BerkeleyEnvironment> env, std::string filename) :
|
||||
WalletDatabase(), env(std::move(env)), strFile(std::move(filename))
|
||||
{
|
||||
auto inserted = this->env->m_databases.emplace(strFile, std::ref(*this));
|
||||
assert(inserted.second);
|
||||
}
|
||||
|
||||
~BerkeleyDatabase() override;
|
||||
|
||||
/** Open the database if it is not already opened. */
|
||||
void Open() override;
|
||||
|
||||
/** Rewrite the entire database on disk, with the exception of key pszSkip if non-zero
|
||||
*/
|
||||
bool Rewrite(const char* pszSkip=nullptr) override;
|
||||
|
||||
/** Indicate the a new database user has began using the database. */
|
||||
void AddRef() override;
|
||||
/** Indicate that database user has stopped using the database and that it could be flushed or closed. */
|
||||
void RemoveRef() override;
|
||||
|
||||
/** Back up the entire database to a file.
|
||||
*/
|
||||
bool Backup(const std::string& strDest) const override;
|
||||
|
||||
/** Make sure all changes are flushed to database file.
|
||||
*/
|
||||
void Flush() override;
|
||||
/** Flush to the database file and close the database.
|
||||
* Also close the environment if no other databases are open in it.
|
||||
*/
|
||||
void Close() override;
|
||||
/* flush the wallet passively (TRY_LOCK)
|
||||
ideal to be called periodically */
|
||||
bool PeriodicFlush() override;
|
||||
|
||||
void IncrementUpdateCounter() override;
|
||||
|
||||
void ReloadDbEnv() override;
|
||||
|
||||
/** Verifies the environment and database file */
|
||||
bool Verify(bilingual_str& error);
|
||||
|
||||
/** Return path to main database filename */
|
||||
std::string Filename() override { return (env->Directory() / strFile).string(); }
|
||||
|
||||
std::string Format() override { return "bdb"; }
|
||||
/**
|
||||
* Pointer to shared database environment.
|
||||
*
|
||||
* Normally there is only one BerkeleyDatabase object per
|
||||
* BerkeleyEnvivonment, but in the special, backwards compatible case where
|
||||
* multiple wallet BDB data files are loaded from the same directory, this
|
||||
* will point to a shared instance that gets freed when the last data file
|
||||
* is closed.
|
||||
*/
|
||||
std::shared_ptr<BerkeleyEnvironment> env;
|
||||
|
||||
/** Database pointer. This is initialized lazily and reset during flushes, so it can be null. */
|
||||
std::unique_ptr<Db> m_db;
|
||||
|
||||
std::string strFile;
|
||||
|
||||
/** Make a BerkeleyBatch connected to this database */
|
||||
std::unique_ptr<DatabaseBatch> MakeBatch(bool flush_on_close = true) override;
|
||||
};
|
||||
|
||||
/** RAII class that provides access to a Berkeley database */
|
||||
class BerkeleyBatch : public DatabaseBatch
|
||||
{
|
||||
/** RAII class that automatically cleanses its data on destruction */
|
||||
class SafeDbt final
|
||||
{
|
||||
Dbt m_dbt;
|
||||
|
||||
public:
|
||||
// construct Dbt with internally-managed data
|
||||
SafeDbt();
|
||||
// construct Dbt with provided data
|
||||
SafeDbt(void* data, size_t size);
|
||||
~SafeDbt();
|
||||
|
||||
// delegate to Dbt
|
||||
const void* get_data() const;
|
||||
u_int32_t get_size() const;
|
||||
|
||||
// conversion operator to access the underlying Dbt
|
||||
operator Dbt*();
|
||||
};
|
||||
|
||||
private:
|
||||
bool ReadKey(CDataStream&& key, CDataStream& value) override;
|
||||
bool WriteKey(CDataStream&& key, CDataStream&& value, bool overwrite = true) override;
|
||||
bool EraseKey(CDataStream&& key) override;
|
||||
bool HasKey(CDataStream&& key) override;
|
||||
|
||||
protected:
|
||||
Db* pdb;
|
||||
std::string strFile;
|
||||
DbTxn* activeTxn;
|
||||
Dbc* m_cursor;
|
||||
bool fReadOnly;
|
||||
bool fFlushOnClose;
|
||||
BerkeleyEnvironment *env;
|
||||
BerkeleyDatabase& m_database;
|
||||
|
||||
public:
|
||||
explicit BerkeleyBatch(BerkeleyDatabase& database, const bool fReadOnly, bool fFlushOnCloseIn=true);
|
||||
~BerkeleyBatch() override;
|
||||
|
||||
BerkeleyBatch(const BerkeleyBatch&) = delete;
|
||||
BerkeleyBatch& operator=(const BerkeleyBatch&) = delete;
|
||||
|
||||
void Flush() override;
|
||||
void Close() override;
|
||||
|
||||
bool StartCursor() override;
|
||||
bool ReadAtCursor(CDataStream& ssKey, CDataStream& ssValue, bool& complete) override;
|
||||
void CloseCursor() override;
|
||||
bool TxnBegin() override;
|
||||
bool TxnCommit() override;
|
||||
bool TxnAbort() override;
|
||||
};
|
||||
|
||||
std::string BerkeleyDatabaseVersion();
|
||||
|
||||
/** Perform sanity check of runtime BDB version versus linked BDB version.
|
||||
*/
|
||||
bool BerkeleyDatabaseSanityCheck();
|
||||
|
||||
//! Return object giving access to Berkeley database at specified path.
|
||||
std::unique_ptr<BerkeleyDatabase> MakeBerkeleyDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error);
|
||||
|
||||
#endif // DIGIBYTE_WALLET_BDB_H
|
||||
13
src/wallet/coincontrol.cpp
Normal file
13
src/wallet/coincontrol.cpp
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) 2018-2019 The Bitcoin Core developers
|
||||
// Copyright (c) 2018-2019 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <wallet/coincontrol.h>
|
||||
|
||||
#include <util/system.h>
|
||||
|
||||
CCoinControl::CCoinControl()
|
||||
{
|
||||
m_avoid_partial_spends = gArgs.GetBoolArg("-avoidpartialspends", DEFAULT_AVOIDPARTIALSPENDS);
|
||||
}
|
||||
95
src/wallet/coincontrol.h
Normal file
95
src/wallet/coincontrol.h
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright (c) 2009-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_COINCONTROL_H
|
||||
#define DIGIBYTE_WALLET_COINCONTROL_H
|
||||
|
||||
#include <outputtype.h>
|
||||
#include <policy/feerate.h>
|
||||
#include <policy/fees.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <script/standard.h>
|
||||
|
||||
#include <optional>
|
||||
#include <limits>
|
||||
|
||||
const int DEFAULT_MIN_DEPTH = 0;
|
||||
const int DEFAULT_MAX_DEPTH = std::numeric_limits<int>::max(); //set max depth to infinity
|
||||
|
||||
//! Default for -avoidpartialspends
|
||||
static constexpr bool DEFAULT_AVOIDPARTIALSPENDS = false;
|
||||
|
||||
/** Coin Control Features. */
|
||||
class CCoinControl
|
||||
{
|
||||
public:
|
||||
//! Custom change destination, if not set an address is generated
|
||||
CTxDestination destChange = CNoDestination();
|
||||
//! Override the default change type if set, ignored if destChange is set
|
||||
std::optional<OutputType> m_change_type;
|
||||
//! If false, only selected inputs are used
|
||||
bool m_add_inputs = true;
|
||||
//! If false, only safe inputs will be used
|
||||
bool m_include_unsafe_inputs = false;
|
||||
//! If false, allows unselected inputs, but requires all selected inputs be used
|
||||
bool fAllowOtherInputs = false;
|
||||
//! Includes watch only addresses which are solvable
|
||||
bool fAllowWatchOnly = false;
|
||||
//! Override automatic min/max checks on fee, m_feerate must be set if true
|
||||
bool fOverrideFeeRate = false;
|
||||
//! Override the wallet's m_pay_tx_fee if set
|
||||
std::optional<CFeeRate> m_feerate;
|
||||
//! Override the default confirmation target if set
|
||||
std::optional<unsigned int> m_confirm_target;
|
||||
//! Override the wallet's m_signal_rbf if set
|
||||
std::optional<bool> m_signal_bip125_rbf;
|
||||
//! Avoid partial use of funds sent to a given address
|
||||
bool m_avoid_partial_spends = DEFAULT_AVOIDPARTIALSPENDS;
|
||||
//! Forbids inclusion of dirty (previously used) addresses
|
||||
bool m_avoid_address_reuse = false;
|
||||
//! Fee estimation mode to control arguments to estimateSmartFee
|
||||
FeeEstimateMode m_fee_mode = FeeEstimateMode::UNSET;
|
||||
//! Minimum chain depth value for coin availability
|
||||
int m_min_depth = DEFAULT_MIN_DEPTH;
|
||||
//! Maximum chain depth value for coin availability
|
||||
int m_max_depth = DEFAULT_MAX_DEPTH;
|
||||
|
||||
CCoinControl();
|
||||
|
||||
bool HasSelected() const
|
||||
{
|
||||
return (setSelected.size() > 0);
|
||||
}
|
||||
|
||||
bool IsSelected(const COutPoint& output) const
|
||||
{
|
||||
return (setSelected.count(output) > 0);
|
||||
}
|
||||
|
||||
void Select(const COutPoint& output)
|
||||
{
|
||||
setSelected.insert(output);
|
||||
}
|
||||
|
||||
void UnSelect(const COutPoint& output)
|
||||
{
|
||||
setSelected.erase(output);
|
||||
}
|
||||
|
||||
void UnSelectAll()
|
||||
{
|
||||
setSelected.clear();
|
||||
}
|
||||
|
||||
void ListSelected(std::vector<COutPoint>& vOutpoints) const
|
||||
{
|
||||
vOutpoints.assign(setSelected.begin(), setSelected.end());
|
||||
}
|
||||
|
||||
private:
|
||||
std::set<COutPoint> setSelected;
|
||||
};
|
||||
|
||||
#endif // DIGIBYTE_WALLET_COINCONTROL_H
|
||||
344
src/wallet/coinselection.cpp
Normal file
344
src/wallet/coinselection.cpp
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
// Copyright (c) 2009-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <wallet/coinselection.h>
|
||||
|
||||
#include <policy/feerate.h>
|
||||
#include <util/system.h>
|
||||
#include <util/moneystr.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
// Descending order comparator
|
||||
struct {
|
||||
bool operator()(const OutputGroup& a, const OutputGroup& b) const
|
||||
{
|
||||
return a.GetSelectionAmount() > b.GetSelectionAmount();
|
||||
}
|
||||
} descending;
|
||||
|
||||
/*
|
||||
* This is the Branch and Bound Coin Selection algorithm designed by Murch. It searches for an input
|
||||
* set that can pay for the spending target and does not exceed the spending target by more than the
|
||||
* cost of creating and spending a change output. The algorithm uses a depth-first search on a binary
|
||||
* tree. In the binary tree, each node corresponds to the inclusion or the omission of a UTXO. UTXOs
|
||||
* are sorted by their effective values and the trees is explored deterministically per the inclusion
|
||||
* branch first. At each node, the algorithm checks whether the selection is within the target range.
|
||||
* While the selection has not reached the target range, more UTXOs are included. When a selection's
|
||||
* value exceeds the target range, the complete subtree deriving from this selection can be omitted.
|
||||
* At that point, the last included UTXO is deselected and the corresponding omission branch explored
|
||||
* instead. The search ends after the complete tree has been searched or after a limited number of tries.
|
||||
*
|
||||
* The search continues to search for better solutions after one solution has been found. The best
|
||||
* solution is chosen by minimizing the waste metric. The waste metric is defined as the cost to
|
||||
* spend the current inputs at the given fee rate minus the long term expected cost to spend the
|
||||
* inputs, plus the amount the selection exceeds the spending target:
|
||||
*
|
||||
* waste = selectionTotal - target + inputs × (currentFeeRate - longTermFeeRate)
|
||||
*
|
||||
* The algorithm uses two additional optimizations. A lookahead keeps track of the total value of
|
||||
* the unexplored UTXOs. A subtree is not explored if the lookahead indicates that the target range
|
||||
* cannot be reached. Further, it is unnecessary to test equivalent combinations. This allows us
|
||||
* to skip testing the inclusion of UTXOs that match the effective value and waste of an omitted
|
||||
* predecessor.
|
||||
*
|
||||
* The Branch and Bound algorithm is described in detail in Murch's Master Thesis:
|
||||
* https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf
|
||||
*
|
||||
* @param const std::vector<CInputCoin>& utxo_pool The set of UTXOs that we are choosing from.
|
||||
* These UTXOs will be sorted in descending order by effective value and the CInputCoins'
|
||||
* values are their effective values.
|
||||
* @param const CAmount& selection_target This is the value that we want to select. It is the lower
|
||||
* bound of the range.
|
||||
* @param const CAmount& cost_of_change This is the cost of creating and spending a change output.
|
||||
* This plus selection_target is the upper bound of the range.
|
||||
* @param std::set<CInputCoin>& out_set -> This is an output parameter for the set of CInputCoins
|
||||
* that have been selected.
|
||||
* @param CAmount& value_ret -> This is an output parameter for the total value of the CInputCoins
|
||||
* that were selected.
|
||||
*/
|
||||
|
||||
static const size_t TOTAL_TRIES = 100000;
|
||||
|
||||
bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret)
|
||||
{
|
||||
out_set.clear();
|
||||
CAmount curr_value = 0;
|
||||
|
||||
std::vector<bool> curr_selection; // select the utxo at this index
|
||||
curr_selection.reserve(utxo_pool.size());
|
||||
|
||||
// Calculate curr_available_value
|
||||
CAmount curr_available_value = 0;
|
||||
for (const OutputGroup& utxo : utxo_pool) {
|
||||
// Assert that this utxo is not negative. It should never be negative, effective value calculation should have removed it
|
||||
assert(utxo.GetSelectionAmount() > 0);
|
||||
curr_available_value += utxo.GetSelectionAmount();
|
||||
}
|
||||
if (curr_available_value < selection_target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sort the utxo_pool
|
||||
std::sort(utxo_pool.begin(), utxo_pool.end(), descending);
|
||||
|
||||
CAmount curr_waste = 0;
|
||||
std::vector<bool> best_selection;
|
||||
CAmount best_waste = MAX_MONEY;
|
||||
|
||||
// Depth First search loop for choosing the UTXOs
|
||||
for (size_t i = 0; i < TOTAL_TRIES; ++i) {
|
||||
// Conditions for starting a backtrack
|
||||
bool backtrack = false;
|
||||
if (curr_value + curr_available_value < selection_target || // Cannot possibly reach target with the amount remaining in the curr_available_value.
|
||||
curr_value > selection_target + cost_of_change || // Selected value is out of range, go back and try other branch
|
||||
(curr_waste > best_waste && (utxo_pool.at(0).fee - utxo_pool.at(0).long_term_fee) > 0)) { // Don't select things which we know will be more wasteful if the waste is increasing
|
||||
backtrack = true;
|
||||
} else if (curr_value >= selection_target) { // Selected value is within range
|
||||
curr_waste += (curr_value - selection_target); // This is the excess value which is added to the waste for the below comparison
|
||||
// Adding another UTXO after this check could bring the waste down if the long term fee is higher than the current fee.
|
||||
// However we are not going to explore that because this optimization for the waste is only done when we have hit our target
|
||||
// value. Adding any more UTXOs will be just burning the UTXO; it will go entirely to fees. Thus we aren't going to
|
||||
// explore any more UTXOs to avoid burning money like that.
|
||||
if (curr_waste <= best_waste) {
|
||||
best_selection = curr_selection;
|
||||
best_selection.resize(utxo_pool.size());
|
||||
best_waste = curr_waste;
|
||||
if (best_waste == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
curr_waste -= (curr_value - selection_target); // Remove the excess value as we will be selecting different coins now
|
||||
backtrack = true;
|
||||
}
|
||||
|
||||
// Backtracking, moving backwards
|
||||
if (backtrack) {
|
||||
// Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
|
||||
while (!curr_selection.empty() && !curr_selection.back()) {
|
||||
curr_selection.pop_back();
|
||||
curr_available_value += utxo_pool.at(curr_selection.size()).GetSelectionAmount();
|
||||
}
|
||||
|
||||
if (curr_selection.empty()) { // We have walked back to the first utxo and no branch is untraversed. All solutions searched
|
||||
break;
|
||||
}
|
||||
|
||||
// Output was included on previous iterations, try excluding now.
|
||||
curr_selection.back() = false;
|
||||
OutputGroup& utxo = utxo_pool.at(curr_selection.size() - 1);
|
||||
curr_value -= utxo.GetSelectionAmount();
|
||||
curr_waste -= utxo.fee - utxo.long_term_fee;
|
||||
} else { // Moving forwards, continuing down this branch
|
||||
OutputGroup& utxo = utxo_pool.at(curr_selection.size());
|
||||
|
||||
// Remove this utxo from the curr_available_value utxo amount
|
||||
curr_available_value -= utxo.GetSelectionAmount();
|
||||
|
||||
// Avoid searching a branch if the previous UTXO has the same value and same waste and was excluded. Since the ratio of fee to
|
||||
// long term fee is the same, we only need to check if one of those values match in order to know that the waste is the same.
|
||||
if (!curr_selection.empty() && !curr_selection.back() &&
|
||||
utxo.GetSelectionAmount() == utxo_pool.at(curr_selection.size() - 1).GetSelectionAmount() &&
|
||||
utxo.fee == utxo_pool.at(curr_selection.size() - 1).fee) {
|
||||
curr_selection.push_back(false);
|
||||
} else {
|
||||
// Inclusion branch first (Largest First Exploration)
|
||||
curr_selection.push_back(true);
|
||||
curr_value += utxo.GetSelectionAmount();
|
||||
curr_waste += utxo.fee - utxo.long_term_fee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for solution
|
||||
if (best_selection.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set output set
|
||||
value_ret = 0;
|
||||
for (size_t i = 0; i < best_selection.size(); ++i) {
|
||||
if (best_selection.at(i)) {
|
||||
util::insert(out_set, utxo_pool.at(i).m_outputs);
|
||||
value_ret += utxo_pool.at(i).m_value;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ApproximateBestSubset(const std::vector<OutputGroup>& groups, const CAmount& nTotalLower, const CAmount& nTargetValue,
|
||||
std::vector<char>& vfBest, CAmount& nBest, int iterations = 1000)
|
||||
{
|
||||
std::vector<char> vfIncluded;
|
||||
|
||||
vfBest.assign(groups.size(), true);
|
||||
nBest = nTotalLower;
|
||||
|
||||
FastRandomContext insecure_rand;
|
||||
|
||||
for (int nRep = 0; nRep < iterations && nBest != nTargetValue; nRep++)
|
||||
{
|
||||
vfIncluded.assign(groups.size(), false);
|
||||
CAmount nTotal = 0;
|
||||
bool fReachedTarget = false;
|
||||
for (int nPass = 0; nPass < 2 && !fReachedTarget; nPass++)
|
||||
{
|
||||
for (unsigned int i = 0; i < groups.size(); i++)
|
||||
{
|
||||
//The solver here uses a randomized algorithm,
|
||||
//the randomness serves no real security purpose but is just
|
||||
//needed to prevent degenerate behavior and it is important
|
||||
//that the rng is fast. We do not use a constant random sequence,
|
||||
//because there may be some privacy improvement by making
|
||||
//the selection random.
|
||||
if (nPass == 0 ? insecure_rand.randbool() : !vfIncluded[i])
|
||||
{
|
||||
nTotal += groups[i].GetSelectionAmount();
|
||||
vfIncluded[i] = true;
|
||||
if (nTotal >= nTargetValue)
|
||||
{
|
||||
fReachedTarget = true;
|
||||
if (nTotal < nBest)
|
||||
{
|
||||
nBest = nTotal;
|
||||
vfBest = vfIncluded;
|
||||
}
|
||||
nTotal -= groups[i].GetSelectionAmount();
|
||||
vfIncluded[i] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& groups, std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet)
|
||||
{
|
||||
setCoinsRet.clear();
|
||||
nValueRet = 0;
|
||||
|
||||
// List of values less than target
|
||||
std::optional<OutputGroup> lowest_larger;
|
||||
std::vector<OutputGroup> applicable_groups;
|
||||
CAmount nTotalLower = 0;
|
||||
|
||||
Shuffle(groups.begin(), groups.end(), FastRandomContext());
|
||||
|
||||
for (const OutputGroup& group : groups) {
|
||||
if (group.GetSelectionAmount() == nTargetValue) {
|
||||
util::insert(setCoinsRet, group.m_outputs);
|
||||
nValueRet += group.m_value;
|
||||
return true;
|
||||
} else if (group.GetSelectionAmount() < nTargetValue + MIN_CHANGE) {
|
||||
applicable_groups.push_back(group);
|
||||
nTotalLower += group.GetSelectionAmount();
|
||||
} else if (!lowest_larger || group.GetSelectionAmount() < lowest_larger->GetSelectionAmount()) {
|
||||
lowest_larger = group;
|
||||
}
|
||||
}
|
||||
|
||||
if (nTotalLower == nTargetValue) {
|
||||
for (const auto& group : applicable_groups) {
|
||||
util::insert(setCoinsRet, group.m_outputs);
|
||||
nValueRet += group.m_value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nTotalLower < nTargetValue) {
|
||||
if (!lowest_larger) return false;
|
||||
util::insert(setCoinsRet, lowest_larger->m_outputs);
|
||||
nValueRet += lowest_larger->m_value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Solve subset sum by stochastic approximation
|
||||
std::sort(applicable_groups.begin(), applicable_groups.end(), descending);
|
||||
std::vector<char> vfBest;
|
||||
CAmount nBest;
|
||||
|
||||
ApproximateBestSubset(applicable_groups, nTotalLower, nTargetValue, vfBest, nBest);
|
||||
if (nBest != nTargetValue && nTotalLower >= nTargetValue + MIN_CHANGE) {
|
||||
ApproximateBestSubset(applicable_groups, nTotalLower, nTargetValue + MIN_CHANGE, vfBest, nBest);
|
||||
}
|
||||
|
||||
// If we have a bigger coin and (either the stochastic approximation didn't find a good solution,
|
||||
// or the next bigger coin is closer), return the bigger coin
|
||||
if (lowest_larger &&
|
||||
((nBest != nTargetValue && nBest < nTargetValue + MIN_CHANGE) || lowest_larger->GetSelectionAmount() <= nBest)) {
|
||||
util::insert(setCoinsRet, lowest_larger->m_outputs);
|
||||
nValueRet += lowest_larger->m_value;
|
||||
} else {
|
||||
for (unsigned int i = 0; i < applicable_groups.size(); i++) {
|
||||
if (vfBest[i]) {
|
||||
util::insert(setCoinsRet, applicable_groups[i].m_outputs);
|
||||
nValueRet += applicable_groups[i].m_value;
|
||||
}
|
||||
}
|
||||
|
||||
if (LogAcceptCategory(BCLog::SELECTCOINS)) {
|
||||
LogPrint(BCLog::SELECTCOINS, "SelectCoins() best subset: "); /* Continued */
|
||||
for (unsigned int i = 0; i < applicable_groups.size(); i++) {
|
||||
if (vfBest[i]) {
|
||||
LogPrint(BCLog::SELECTCOINS, "%s ", FormatMoney(applicable_groups[i].m_value)); /* Continued */
|
||||
}
|
||||
}
|
||||
LogPrint(BCLog::SELECTCOINS, "total %s\n", FormatMoney(nBest));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
|
||||
OutputGroup
|
||||
|
||||
******************************************************************************/
|
||||
|
||||
void OutputGroup::Insert(const CInputCoin& output, int depth, bool from_me, size_t ancestors, size_t descendants, bool positive_only) {
|
||||
// Compute the effective value first
|
||||
const CAmount coin_fee = output.m_input_bytes < 0 ? 0 : m_effective_feerate.GetFee(output.m_input_bytes);
|
||||
const CAmount ev = output.txout.nValue - coin_fee;
|
||||
|
||||
// Filter for positive only here before adding the coin
|
||||
if (positive_only && ev <= 0) return;
|
||||
|
||||
m_outputs.push_back(output);
|
||||
CInputCoin& coin = m_outputs.back();
|
||||
|
||||
coin.m_fee = coin_fee;
|
||||
fee += coin.m_fee;
|
||||
|
||||
coin.m_long_term_fee = coin.m_input_bytes < 0 ? 0 : m_long_term_feerate.GetFee(coin.m_input_bytes);
|
||||
long_term_fee += coin.m_long_term_fee;
|
||||
|
||||
coin.effective_value = ev;
|
||||
effective_value += coin.effective_value;
|
||||
|
||||
m_from_me &= from_me;
|
||||
m_value += output.txout.nValue;
|
||||
m_depth = std::min(m_depth, depth);
|
||||
// ancestors here express the number of ancestors the new coin will end up having, which is
|
||||
// the sum, rather than the max; this will overestimate in the cases where multiple inputs
|
||||
// have common ancestors
|
||||
m_ancestors += ancestors;
|
||||
// descendants is the count as seen from the top ancestor, not the descendants as seen from the
|
||||
// coin itself; thus, this value is counted as the max, not the sum
|
||||
m_descendants = std::max(m_descendants, descendants);
|
||||
}
|
||||
|
||||
bool OutputGroup::EligibleForSpending(const CoinEligibilityFilter& eligibility_filter) const
|
||||
{
|
||||
return m_depth >= (m_from_me ? eligibility_filter.conf_mine : eligibility_filter.conf_theirs)
|
||||
&& m_ancestors <= eligibility_filter.max_ancestors
|
||||
&& m_descendants <= eligibility_filter.max_descendants;
|
||||
}
|
||||
|
||||
CAmount OutputGroup::GetSelectionAmount() const
|
||||
{
|
||||
return m_subtract_fee_outputs ? m_value : effective_value;
|
||||
}
|
||||
175
src/wallet/coinselection.h
Normal file
175
src/wallet/coinselection.h
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
// Copyright (c) 2017-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2017-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_COINSELECTION_H
|
||||
#define DIGIBYTE_WALLET_COINSELECTION_H
|
||||
|
||||
#include <amount.h>
|
||||
#include <policy/feerate.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <random.h>
|
||||
|
||||
//! target minimum change amount
|
||||
static constexpr CAmount MIN_CHANGE{COIN / 100};
|
||||
//! final minimum change amount after paying for fees
|
||||
static const CAmount MIN_FINAL_CHANGE = MIN_CHANGE/2;
|
||||
|
||||
/** A UTXO under consideration for use in funding a new transaction. */
|
||||
class CInputCoin {
|
||||
public:
|
||||
CInputCoin(const CTransactionRef& tx, unsigned int i)
|
||||
{
|
||||
if (!tx)
|
||||
throw std::invalid_argument("tx should not be null");
|
||||
if (i >= tx->vout.size())
|
||||
throw std::out_of_range("The output index is out of range");
|
||||
|
||||
outpoint = COutPoint(tx->GetHash(), i);
|
||||
txout = tx->vout[i];
|
||||
effective_value = txout.nValue;
|
||||
}
|
||||
|
||||
CInputCoin(const CTransactionRef& tx, unsigned int i, int input_bytes) : CInputCoin(tx, i)
|
||||
{
|
||||
m_input_bytes = input_bytes;
|
||||
}
|
||||
|
||||
COutPoint outpoint;
|
||||
CTxOut txout;
|
||||
CAmount effective_value;
|
||||
CAmount m_fee{0};
|
||||
CAmount m_long_term_fee{0};
|
||||
|
||||
/** Pre-computed estimated size of this output as a fully-signed input in a transaction. Can be -1 if it could not be calculated */
|
||||
int m_input_bytes{-1};
|
||||
|
||||
bool operator<(const CInputCoin& rhs) const {
|
||||
return outpoint < rhs.outpoint;
|
||||
}
|
||||
|
||||
bool operator!=(const CInputCoin& rhs) const {
|
||||
return outpoint != rhs.outpoint;
|
||||
}
|
||||
|
||||
bool operator==(const CInputCoin& rhs) const {
|
||||
return outpoint == rhs.outpoint;
|
||||
}
|
||||
};
|
||||
|
||||
/** Parameters for one iteration of Coin Selection. */
|
||||
struct CoinSelectionParams
|
||||
{
|
||||
/** Size of a change output in bytes, determined by the output type. */
|
||||
size_t change_output_size = 0;
|
||||
/** Size of the input to spend a change output in virtual bytes. */
|
||||
size_t change_spend_size = 0;
|
||||
/** Cost of creating the change output. */
|
||||
CAmount m_change_fee{0};
|
||||
/** Cost of creating the change output + cost of spending the change output in the future. */
|
||||
CAmount m_cost_of_change{0};
|
||||
/** The targeted feerate of the transaction being built. */
|
||||
CFeeRate m_effective_feerate;
|
||||
/** The feerate estimate used to estimate an upper bound on what should be sufficient to spend
|
||||
* the change output sometime in the future. */
|
||||
CFeeRate m_long_term_feerate;
|
||||
/** If the cost to spend a change output at the discard feerate exceeds its value, drop it to fees. */
|
||||
CFeeRate m_discard_feerate;
|
||||
/** Size of the transaction before coin selection, consisting of the header and recipient
|
||||
* output(s), excluding the inputs and change output(s). */
|
||||
size_t tx_noinputs_size = 0;
|
||||
/** Indicate that we are subtracting the fee from outputs */
|
||||
bool m_subtract_fee_outputs = false;
|
||||
/** When true, always spend all (up to OUTPUT_GROUP_MAX_ENTRIES) or none of the outputs
|
||||
* associated with the same address. This helps reduce privacy leaks resulting from address
|
||||
* reuse. Dust outputs are not eligible to be added to output groups and thus not considered. */
|
||||
bool m_avoid_partial_spends = false;
|
||||
|
||||
CoinSelectionParams(size_t change_output_size, size_t change_spend_size, CFeeRate effective_feerate,
|
||||
CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial) :
|
||||
change_output_size(change_output_size),
|
||||
change_spend_size(change_spend_size),
|
||||
m_effective_feerate(effective_feerate),
|
||||
m_long_term_feerate(long_term_feerate),
|
||||
m_discard_feerate(discard_feerate),
|
||||
tx_noinputs_size(tx_noinputs_size),
|
||||
m_avoid_partial_spends(avoid_partial)
|
||||
{}
|
||||
CoinSelectionParams() {}
|
||||
};
|
||||
|
||||
/** Parameters for filtering which OutputGroups we may use in coin selection.
|
||||
* We start by being very selective and requiring multiple confirmations and
|
||||
* then get more permissive if we cannot fund the transaction. */
|
||||
struct CoinEligibilityFilter
|
||||
{
|
||||
/** Minimum number of confirmations for outputs that we sent to ourselves.
|
||||
* We may use unconfirmed UTXOs sent from ourselves, e.g. change outputs. */
|
||||
const int conf_mine;
|
||||
/** Minimum number of confirmations for outputs received from a different wallet. */
|
||||
const int conf_theirs;
|
||||
/** Maximum number of unconfirmed ancestors aggregated across all UTXOs in an OutputGroup. */
|
||||
const uint64_t max_ancestors;
|
||||
/** Maximum number of descendants that a single UTXO in the OutputGroup may have. */
|
||||
const uint64_t max_descendants;
|
||||
/** When avoid_reuse=true and there are full groups (OUTPUT_GROUP_MAX_ENTRIES), whether or not to use any partial groups.*/
|
||||
const bool m_include_partial_groups{false};
|
||||
|
||||
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_descendants(max_ancestors) {}
|
||||
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors, uint64_t max_descendants) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_descendants(max_descendants) {}
|
||||
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors, uint64_t max_descendants, bool include_partial) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_descendants(max_descendants), m_include_partial_groups(include_partial) {}
|
||||
};
|
||||
|
||||
/** A group of UTXOs paid to the same output script. */
|
||||
struct OutputGroup
|
||||
{
|
||||
/** The list of UTXOs contained in this output group. */
|
||||
std::vector<CInputCoin> m_outputs;
|
||||
/** Whether the UTXOs were sent by the wallet to itself. This is relevant because we may want at
|
||||
* least a certain number of confirmations on UTXOs received from outside wallets while trusting
|
||||
* our own UTXOs more. */
|
||||
bool m_from_me{true};
|
||||
/** The total value of the UTXOs in sum. */
|
||||
CAmount m_value{0};
|
||||
/** The minimum number of confirmations the UTXOs in the group have. Unconfirmed is 0. */
|
||||
int m_depth{999};
|
||||
/** The aggregated count of unconfirmed ancestors of all UTXOs in this
|
||||
* group. Not deduplicated and may overestimate when ancestors are shared. */
|
||||
size_t m_ancestors{0};
|
||||
/** The maximum count of descendants of a single UTXO in this output group. */
|
||||
size_t m_descendants{0};
|
||||
/** The value of the UTXOs after deducting the cost of spending them at the effective feerate. */
|
||||
CAmount effective_value{0};
|
||||
/** The fee to spend these UTXOs at the effective feerate. */
|
||||
CAmount fee{0};
|
||||
/** The target feerate of the transaction we're trying to build. */
|
||||
CFeeRate m_effective_feerate{0};
|
||||
/** The fee to spend these UTXOs at the long term feerate. */
|
||||
CAmount long_term_fee{0};
|
||||
/** The feerate for spending a created change output eventually (i.e. not urgently, and thus at
|
||||
* a lower feerate). Calculated using long term fee estimate. This is used to decide whether
|
||||
* it could be economical to create a change output. */
|
||||
CFeeRate m_long_term_feerate{0};
|
||||
/** Indicate that we are subtracting the fee from outputs.
|
||||
* When true, the value that is used for coin selection is the UTXO's real value rather than effective value */
|
||||
bool m_subtract_fee_outputs{false};
|
||||
|
||||
OutputGroup() {}
|
||||
OutputGroup(const CoinSelectionParams& params) :
|
||||
m_effective_feerate(params.m_effective_feerate),
|
||||
m_long_term_feerate(params.m_long_term_feerate),
|
||||
m_subtract_fee_outputs(params.m_subtract_fee_outputs)
|
||||
{}
|
||||
|
||||
void Insert(const CInputCoin& output, int depth, bool from_me, size_t ancestors, size_t descendants, bool positive_only);
|
||||
bool EligibleForSpending(const CoinEligibilityFilter& eligibility_filter) const;
|
||||
CAmount GetSelectionAmount() const;
|
||||
};
|
||||
|
||||
bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret);
|
||||
|
||||
// Original coin selection algorithm as a fallback
|
||||
bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& groups, std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet);
|
||||
|
||||
#endif // DIGIBYTE_WALLET_COINSELECTION_H
|
||||
8
src/wallet/context.cpp
Normal file
8
src/wallet/context.cpp
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <wallet/context.h>
|
||||
|
||||
WalletContext::WalletContext() {}
|
||||
WalletContext::~WalletContext() {}
|
||||
34
src/wallet/context.h
Normal file
34
src/wallet/context.h
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) 2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_CONTEXT_H
|
||||
#define DIGIBYTE_WALLET_CONTEXT_H
|
||||
|
||||
class ArgsManager;
|
||||
namespace interfaces {
|
||||
class Chain;
|
||||
} // namespace interfaces
|
||||
|
||||
//! WalletContext struct containing references to state shared between CWallet
|
||||
//! instances, like the reference to the chain interface, and the list of opened
|
||||
//! wallets.
|
||||
//!
|
||||
//! Future shared state can be added here as an alternative to adding global
|
||||
//! variables.
|
||||
//!
|
||||
//! The struct isn't intended to have any member functions. It should just be a
|
||||
//! collection of state pointers that doesn't pull in dependencies or implement
|
||||
//! behavior.
|
||||
struct WalletContext {
|
||||
interfaces::Chain* chain{nullptr};
|
||||
ArgsManager* args{nullptr};
|
||||
|
||||
//! Declare default constructor and destructor that are not inline, so code
|
||||
//! instantiating the WalletContext struct doesn't need to #include class
|
||||
//! definitions for smart pointer and container members.
|
||||
WalletContext();
|
||||
~WalletContext();
|
||||
};
|
||||
|
||||
#endif // DIGIBYTE_WALLET_CONTEXT_H
|
||||
139
src/wallet/crypter.cpp
Normal file
139
src/wallet/crypter.cpp
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// Copyright (c) 2009-2019 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2019 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <wallet/crypter.h>
|
||||
|
||||
#include <crypto/aes.h>
|
||||
#include <crypto/sha512.h>
|
||||
#include <util/system.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
int CCrypter::BytesToKeySHA512AES(const std::vector<unsigned char>& chSalt, const SecureString& strKeyData, int count, unsigned char *key,unsigned char *iv) const
|
||||
{
|
||||
// This mimics the behavior of openssl's EVP_BytesToKey with an aes256cbc
|
||||
// cipher and sha512 message digest. Because sha512's output size (64b) is
|
||||
// greater than the aes256 block size (16b) + aes256 key size (32b),
|
||||
// there's no need to process more than once (D_0).
|
||||
|
||||
if(!count || !key || !iv)
|
||||
return 0;
|
||||
|
||||
unsigned char buf[CSHA512::OUTPUT_SIZE];
|
||||
CSHA512 di;
|
||||
|
||||
di.Write((const unsigned char*)strKeyData.data(), strKeyData.size());
|
||||
di.Write(chSalt.data(), chSalt.size());
|
||||
di.Finalize(buf);
|
||||
|
||||
for(int i = 0; i != count - 1; i++)
|
||||
di.Reset().Write(buf, sizeof(buf)).Finalize(buf);
|
||||
|
||||
memcpy(key, buf, WALLET_CRYPTO_KEY_SIZE);
|
||||
memcpy(iv, buf + WALLET_CRYPTO_KEY_SIZE, WALLET_CRYPTO_IV_SIZE);
|
||||
memory_cleanse(buf, sizeof(buf));
|
||||
return WALLET_CRYPTO_KEY_SIZE;
|
||||
}
|
||||
|
||||
bool CCrypter::SetKeyFromPassphrase(const SecureString& strKeyData, const std::vector<unsigned char>& chSalt, const unsigned int nRounds, const unsigned int nDerivationMethod)
|
||||
{
|
||||
if (nRounds < 1 || chSalt.size() != WALLET_CRYPTO_SALT_SIZE)
|
||||
return false;
|
||||
|
||||
int i = 0;
|
||||
if (nDerivationMethod == 0)
|
||||
i = BytesToKeySHA512AES(chSalt, strKeyData, nRounds, vchKey.data(), vchIV.data());
|
||||
|
||||
if (i != (int)WALLET_CRYPTO_KEY_SIZE)
|
||||
{
|
||||
memory_cleanse(vchKey.data(), vchKey.size());
|
||||
memory_cleanse(vchIV.data(), vchIV.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
fKeySet = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CCrypter::SetKey(const CKeyingMaterial& chNewKey, const std::vector<unsigned char>& chNewIV)
|
||||
{
|
||||
if (chNewKey.size() != WALLET_CRYPTO_KEY_SIZE || chNewIV.size() != WALLET_CRYPTO_IV_SIZE)
|
||||
return false;
|
||||
|
||||
memcpy(vchKey.data(), chNewKey.data(), chNewKey.size());
|
||||
memcpy(vchIV.data(), chNewIV.data(), chNewIV.size());
|
||||
|
||||
fKeySet = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CCrypter::Encrypt(const CKeyingMaterial& vchPlaintext, std::vector<unsigned char> &vchCiphertext) const
|
||||
{
|
||||
if (!fKeySet)
|
||||
return false;
|
||||
|
||||
// max ciphertext len for a n bytes of plaintext is
|
||||
// n + AES_BLOCKSIZE bytes
|
||||
vchCiphertext.resize(vchPlaintext.size() + AES_BLOCKSIZE);
|
||||
|
||||
AES256CBCEncrypt enc(vchKey.data(), vchIV.data(), true);
|
||||
size_t nLen = enc.Encrypt(vchPlaintext.data(), vchPlaintext.size(), vchCiphertext.data());
|
||||
if(nLen < vchPlaintext.size())
|
||||
return false;
|
||||
vchCiphertext.resize(nLen);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CCrypter::Decrypt(const std::vector<unsigned char>& vchCiphertext, CKeyingMaterial& vchPlaintext) const
|
||||
{
|
||||
if (!fKeySet)
|
||||
return false;
|
||||
|
||||
// plaintext will always be equal to or lesser than length of ciphertext
|
||||
int nLen = vchCiphertext.size();
|
||||
|
||||
vchPlaintext.resize(nLen);
|
||||
|
||||
AES256CBCDecrypt dec(vchKey.data(), vchIV.data(), true);
|
||||
nLen = dec.Decrypt(vchCiphertext.data(), vchCiphertext.size(), vchPlaintext.data());
|
||||
if(nLen == 0)
|
||||
return false;
|
||||
vchPlaintext.resize(nLen);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EncryptSecret(const CKeyingMaterial& vMasterKey, const CKeyingMaterial &vchPlaintext, const uint256& nIV, std::vector<unsigned char> &vchCiphertext)
|
||||
{
|
||||
CCrypter cKeyCrypter;
|
||||
std::vector<unsigned char> chIV(WALLET_CRYPTO_IV_SIZE);
|
||||
memcpy(chIV.data(), &nIV, WALLET_CRYPTO_IV_SIZE);
|
||||
if(!cKeyCrypter.SetKey(vMasterKey, chIV))
|
||||
return false;
|
||||
return cKeyCrypter.Encrypt(*((const CKeyingMaterial*)&vchPlaintext), vchCiphertext);
|
||||
}
|
||||
|
||||
bool DecryptSecret(const CKeyingMaterial& vMasterKey, const std::vector<unsigned char>& vchCiphertext, const uint256& nIV, CKeyingMaterial& vchPlaintext)
|
||||
{
|
||||
CCrypter cKeyCrypter;
|
||||
std::vector<unsigned char> chIV(WALLET_CRYPTO_IV_SIZE);
|
||||
memcpy(chIV.data(), &nIV, WALLET_CRYPTO_IV_SIZE);
|
||||
if(!cKeyCrypter.SetKey(vMasterKey, chIV))
|
||||
return false;
|
||||
return cKeyCrypter.Decrypt(vchCiphertext, vchPlaintext);
|
||||
}
|
||||
|
||||
bool DecryptKey(const CKeyingMaterial& vMasterKey, const std::vector<unsigned char>& vchCryptedSecret, const CPubKey& vchPubKey, CKey& key)
|
||||
{
|
||||
CKeyingMaterial vchSecret;
|
||||
if(!DecryptSecret(vMasterKey, vchCryptedSecret, vchPubKey.GetHash(), vchSecret))
|
||||
return false;
|
||||
|
||||
if (vchSecret.size() != 32)
|
||||
return false;
|
||||
|
||||
key.Set(vchSecret.begin(), vchSecret.end(), vchPubKey.IsCompressed());
|
||||
return key.VerifyPubKey(vchPubKey);
|
||||
}
|
||||
110
src/wallet/crypter.h
Normal file
110
src/wallet/crypter.h
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// Copyright (c) 2009-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_CRYPTER_H
|
||||
#define DIGIBYTE_WALLET_CRYPTER_H
|
||||
|
||||
#include <serialize.h>
|
||||
#include <support/allocators/secure.h>
|
||||
#include <script/signingprovider.h>
|
||||
|
||||
|
||||
const unsigned int WALLET_CRYPTO_KEY_SIZE = 32;
|
||||
const unsigned int WALLET_CRYPTO_SALT_SIZE = 8;
|
||||
const unsigned int WALLET_CRYPTO_IV_SIZE = 16;
|
||||
|
||||
/**
|
||||
* Private key encryption is done based on a CMasterKey,
|
||||
* which holds a salt and random encryption key.
|
||||
*
|
||||
* CMasterKeys are encrypted using AES-256-CBC using a key
|
||||
* derived using derivation method nDerivationMethod
|
||||
* (0 == EVP_sha512()) and derivation iterations nDeriveIterations.
|
||||
* vchOtherDerivationParameters is provided for alternative algorithms
|
||||
* which may require more parameters (such as scrypt).
|
||||
*
|
||||
* Wallet Private Keys are then encrypted using AES-256-CBC
|
||||
* with the double-sha256 of the public key as the IV, and the
|
||||
* master key's key as the encryption key (see keystore.[ch]).
|
||||
*/
|
||||
|
||||
/** Master key for wallet encryption */
|
||||
class CMasterKey
|
||||
{
|
||||
public:
|
||||
std::vector<unsigned char> vchCryptedKey;
|
||||
std::vector<unsigned char> vchSalt;
|
||||
//! 0 = EVP_sha512()
|
||||
//! 1 = scrypt()
|
||||
unsigned int nDerivationMethod;
|
||||
unsigned int nDeriveIterations;
|
||||
//! Use this for more parameters to key derivation,
|
||||
//! such as the various parameters to scrypt
|
||||
std::vector<unsigned char> vchOtherDerivationParameters;
|
||||
|
||||
SERIALIZE_METHODS(CMasterKey, obj)
|
||||
{
|
||||
READWRITE(obj.vchCryptedKey, obj.vchSalt, obj.nDerivationMethod, obj.nDeriveIterations, obj.vchOtherDerivationParameters);
|
||||
}
|
||||
|
||||
CMasterKey()
|
||||
{
|
||||
// 25000 rounds is just under 0.1 seconds on a 1.86 GHz Pentium M
|
||||
// ie slightly lower than the lowest hardware we need bother supporting
|
||||
nDeriveIterations = 25000;
|
||||
nDerivationMethod = 0;
|
||||
vchOtherDerivationParameters = std::vector<unsigned char>(0);
|
||||
}
|
||||
};
|
||||
|
||||
typedef std::vector<unsigned char, secure_allocator<unsigned char> > CKeyingMaterial;
|
||||
|
||||
namespace wallet_crypto_tests
|
||||
{
|
||||
class TestCrypter;
|
||||
}
|
||||
|
||||
/** Encryption/decryption context with key information */
|
||||
class CCrypter
|
||||
{
|
||||
friend class wallet_crypto_tests::TestCrypter; // for test access to chKey/chIV
|
||||
private:
|
||||
std::vector<unsigned char, secure_allocator<unsigned char>> vchKey;
|
||||
std::vector<unsigned char, secure_allocator<unsigned char>> vchIV;
|
||||
bool fKeySet;
|
||||
|
||||
int BytesToKeySHA512AES(const std::vector<unsigned char>& chSalt, const SecureString& strKeyData, int count, unsigned char *key,unsigned char *iv) const;
|
||||
|
||||
public:
|
||||
bool SetKeyFromPassphrase(const SecureString &strKeyData, const std::vector<unsigned char>& chSalt, const unsigned int nRounds, const unsigned int nDerivationMethod);
|
||||
bool Encrypt(const CKeyingMaterial& vchPlaintext, std::vector<unsigned char> &vchCiphertext) const;
|
||||
bool Decrypt(const std::vector<unsigned char>& vchCiphertext, CKeyingMaterial& vchPlaintext) const;
|
||||
bool SetKey(const CKeyingMaterial& chNewKey, const std::vector<unsigned char>& chNewIV);
|
||||
|
||||
void CleanKey()
|
||||
{
|
||||
memory_cleanse(vchKey.data(), vchKey.size());
|
||||
memory_cleanse(vchIV.data(), vchIV.size());
|
||||
fKeySet = false;
|
||||
}
|
||||
|
||||
CCrypter()
|
||||
{
|
||||
fKeySet = false;
|
||||
vchKey.resize(WALLET_CRYPTO_KEY_SIZE);
|
||||
vchIV.resize(WALLET_CRYPTO_IV_SIZE);
|
||||
}
|
||||
|
||||
~CCrypter()
|
||||
{
|
||||
CleanKey();
|
||||
}
|
||||
};
|
||||
|
||||
bool EncryptSecret(const CKeyingMaterial& vMasterKey, const CKeyingMaterial &vchPlaintext, const uint256& nIV, std::vector<unsigned char> &vchCiphertext);
|
||||
bool DecryptSecret(const CKeyingMaterial& vMasterKey, const std::vector<unsigned char>& vchCiphertext, const uint256& nIV, CKeyingMaterial& vchPlaintext);
|
||||
bool DecryptKey(const CKeyingMaterial& vMasterKey, const std::vector<unsigned char>& vchCryptedSecret, const CPubKey& vchPubKey, CKey& key);
|
||||
|
||||
#endif // DIGIBYTE_WALLET_CRYPTER_H
|
||||
138
src/wallet/db.cpp
Normal file
138
src/wallet/db.cpp
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <chainparams.h>
|
||||
#include <fs.h>
|
||||
#include <logging.h>
|
||||
#include <wallet/db.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
std::vector<fs::path> ListDatabases(const fs::path& wallet_dir)
|
||||
{
|
||||
const size_t offset = wallet_dir.string().size() + (wallet_dir == wallet_dir.root_name() ? 0 : 1);
|
||||
std::vector<fs::path> paths;
|
||||
boost::system::error_code ec;
|
||||
|
||||
for (auto it = fs::recursive_directory_iterator(wallet_dir, ec); it != fs::recursive_directory_iterator(); it.increment(ec)) {
|
||||
if (ec) {
|
||||
if (fs::is_directory(*it)) {
|
||||
it.disable_recursion_pending();
|
||||
LogPrintf("%s: %s %s -- skipping.\n", __func__, ec.message(), it->path().string());
|
||||
} else {
|
||||
LogPrintf("%s: %s %s\n", __func__, ec.message(), it->path().string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get wallet path relative to walletdir by removing walletdir from the wallet path.
|
||||
// This can be replaced by boost::filesystem::lexically_relative once boost is bumped to 1.60.
|
||||
const fs::path path = it->path().string().substr(offset);
|
||||
|
||||
if (it->status().type() == fs::directory_file &&
|
||||
(IsBDBFile(BDBDataFile(it->path())) || IsSQLiteFile(SQLiteDataFile(it->path())))) {
|
||||
// Found a directory which contains wallet.dat btree file, add it as a wallet.
|
||||
paths.emplace_back(path);
|
||||
} else if (it.depth() == 0 && it->symlink_status().type() == fs::regular_file && IsBDBFile(it->path())) {
|
||||
if (it->path().filename() == "wallet.dat") {
|
||||
// Found top-level wallet.dat btree file, add top level directory ""
|
||||
// as a wallet.
|
||||
paths.emplace_back();
|
||||
} else {
|
||||
// Found top-level btree file not called wallet.dat. Current Vexta
|
||||
// software will never create these files but will allow them to be
|
||||
// opened in a shared database environment for backwards compatibility.
|
||||
// Add it to the list of available wallets.
|
||||
paths.emplace_back(path);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LogPrintf("%s: Error scanning %s: %s\n", __func__, it->path().string(), e.what());
|
||||
it.disable_recursion_pending();
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
fs::path BDBDataFile(const fs::path& wallet_path)
|
||||
{
|
||||
if (fs::is_regular_file(wallet_path)) {
|
||||
// Special case for backwards compatibility: if wallet path points to an
|
||||
// existing file, treat it as the path to a BDB data file in a parent
|
||||
// directory that also contains BDB log files.
|
||||
return wallet_path;
|
||||
} else {
|
||||
// Normal case: Interpret wallet path as a directory path containing
|
||||
// data and log files.
|
||||
return wallet_path / "wallet.dat";
|
||||
}
|
||||
}
|
||||
|
||||
fs::path SQLiteDataFile(const fs::path& path)
|
||||
{
|
||||
return path / "wallet.dat";
|
||||
}
|
||||
|
||||
bool IsBDBFile(const fs::path& path)
|
||||
{
|
||||
if (!fs::exists(path)) return false;
|
||||
|
||||
// A Berkeley DB Btree file has at least 4K.
|
||||
// This check also prevents opening lock files.
|
||||
boost::system::error_code ec;
|
||||
auto size = fs::file_size(path, ec);
|
||||
if (ec) LogPrintf("%s: %s %s\n", __func__, ec.message(), path.string());
|
||||
if (size < 4096) return false;
|
||||
|
||||
fsbridge::ifstream file(path, std::ios::binary);
|
||||
if (!file.is_open()) return false;
|
||||
|
||||
file.seekg(12, std::ios::beg); // Magic bytes start at offset 12
|
||||
uint32_t data = 0;
|
||||
file.read((char*) &data, sizeof(data)); // Read 4 bytes of file to compare against magic
|
||||
|
||||
// Berkeley DB Btree magic bytes, from:
|
||||
// https://github.com/file/file/blob/5824af38469ec1ca9ac3ffd251e7afe9dc11e227/magic/Magdir/database#L74-L75
|
||||
// - big endian systems - 00 05 31 62
|
||||
// - little endian systems - 62 31 05 00
|
||||
return data == 0x00053162 || data == 0x62310500;
|
||||
}
|
||||
|
||||
bool IsSQLiteFile(const fs::path& path)
|
||||
{
|
||||
if (!fs::exists(path)) return false;
|
||||
|
||||
// A SQLite Database file is at least 512 bytes.
|
||||
boost::system::error_code ec;
|
||||
auto size = fs::file_size(path, ec);
|
||||
if (ec) LogPrintf("%s: %s %s\n", __func__, ec.message(), path.string());
|
||||
if (size < 512) return false;
|
||||
|
||||
fsbridge::ifstream file(path, std::ios::binary);
|
||||
if (!file.is_open()) return false;
|
||||
|
||||
// Magic is at beginning and is 16 bytes long
|
||||
char magic[16];
|
||||
file.read(magic, 16);
|
||||
|
||||
// Application id is at offset 68 and 4 bytes long
|
||||
file.seekg(68, std::ios::beg);
|
||||
char app_id[4];
|
||||
file.read(app_id, 4);
|
||||
|
||||
file.close();
|
||||
|
||||
// Check the magic, see https://sqlite.org/fileformat2.html
|
||||
std::string magic_str(magic, 16);
|
||||
if (magic_str != std::string("SQLite format 3", 16)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the application id matches our network magic
|
||||
return memcmp(Params().MessageStart(), app_id, 4) == 0;
|
||||
}
|
||||
236
src/wallet/db.h
Normal file
236
src/wallet/db.h
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_DB_H
|
||||
#define DIGIBYTE_WALLET_DB_H
|
||||
|
||||
#include <clientversion.h>
|
||||
#include <fs.h>
|
||||
#include <streams.h>
|
||||
#include <support/allocators/secure.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
struct bilingual_str;
|
||||
|
||||
void SplitWalletPath(const fs::path& wallet_path, fs::path& env_directory, std::string& database_filename);
|
||||
|
||||
/** RAII class that provides access to a WalletDatabase */
|
||||
class DatabaseBatch
|
||||
{
|
||||
private:
|
||||
virtual bool ReadKey(CDataStream&& key, CDataStream& value) = 0;
|
||||
virtual bool WriteKey(CDataStream&& key, CDataStream&& value, bool overwrite=true) = 0;
|
||||
virtual bool EraseKey(CDataStream&& key) = 0;
|
||||
virtual bool HasKey(CDataStream&& key) = 0;
|
||||
|
||||
public:
|
||||
explicit DatabaseBatch() {}
|
||||
virtual ~DatabaseBatch() {}
|
||||
|
||||
DatabaseBatch(const DatabaseBatch&) = delete;
|
||||
DatabaseBatch& operator=(const DatabaseBatch&) = delete;
|
||||
|
||||
virtual void Flush() = 0;
|
||||
virtual void Close() = 0;
|
||||
|
||||
template <typename K, typename T>
|
||||
bool Read(const K& key, T& value)
|
||||
{
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
ssKey.reserve(1000);
|
||||
ssKey << key;
|
||||
|
||||
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
|
||||
if (!ReadKey(std::move(ssKey), ssValue)) return false;
|
||||
try {
|
||||
ssValue >> value;
|
||||
return true;
|
||||
} catch (const std::exception&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename K, typename T>
|
||||
bool Write(const K& key, const T& value, bool fOverwrite = true)
|
||||
{
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
ssKey.reserve(1000);
|
||||
ssKey << key;
|
||||
|
||||
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
|
||||
ssValue.reserve(10000);
|
||||
ssValue << value;
|
||||
|
||||
return WriteKey(std::move(ssKey), std::move(ssValue), fOverwrite);
|
||||
}
|
||||
|
||||
template <typename K>
|
||||
bool Erase(const K& key)
|
||||
{
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
ssKey.reserve(1000);
|
||||
ssKey << key;
|
||||
|
||||
return EraseKey(std::move(ssKey));
|
||||
}
|
||||
|
||||
template <typename K>
|
||||
bool Exists(const K& key)
|
||||
{
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
ssKey.reserve(1000);
|
||||
ssKey << key;
|
||||
|
||||
return HasKey(std::move(ssKey));
|
||||
}
|
||||
|
||||
virtual bool StartCursor() = 0;
|
||||
virtual bool ReadAtCursor(CDataStream& ssKey, CDataStream& ssValue, bool& complete) = 0;
|
||||
virtual void CloseCursor() = 0;
|
||||
virtual bool TxnBegin() = 0;
|
||||
virtual bool TxnCommit() = 0;
|
||||
virtual bool TxnAbort() = 0;
|
||||
};
|
||||
|
||||
/** An instance of this class represents one database.
|
||||
**/
|
||||
class WalletDatabase
|
||||
{
|
||||
public:
|
||||
/** Create dummy DB handle */
|
||||
WalletDatabase() : nUpdateCounter(0), nLastSeen(0), nLastFlushed(0), nLastWalletUpdate(0) {}
|
||||
virtual ~WalletDatabase() {};
|
||||
|
||||
/** Open the database if it is not already opened. */
|
||||
virtual void Open() = 0;
|
||||
|
||||
//! Counts the number of active database users to be sure that the database is not closed while someone is using it
|
||||
std::atomic<int> m_refcount{0};
|
||||
/** Indicate the a new database user has began using the database. Increments m_refcount */
|
||||
virtual void AddRef() = 0;
|
||||
/** Indicate that database user has stopped using the database and that it could be flushed or closed. Decrement m_refcount */
|
||||
virtual void RemoveRef() = 0;
|
||||
|
||||
/** Rewrite the entire database on disk, with the exception of key pszSkip if non-zero
|
||||
*/
|
||||
virtual bool Rewrite(const char* pszSkip=nullptr) = 0;
|
||||
|
||||
/** Back up the entire database to a file.
|
||||
*/
|
||||
virtual bool Backup(const std::string& strDest) const = 0;
|
||||
|
||||
/** Make sure all changes are flushed to database file.
|
||||
*/
|
||||
virtual void Flush() = 0;
|
||||
/** Flush to the database file and close the database.
|
||||
* Also close the environment if no other databases are open in it.
|
||||
*/
|
||||
virtual void Close() = 0;
|
||||
/* flush the wallet passively (TRY_LOCK)
|
||||
ideal to be called periodically */
|
||||
virtual bool PeriodicFlush() = 0;
|
||||
|
||||
virtual void IncrementUpdateCounter() = 0;
|
||||
|
||||
virtual void ReloadDbEnv() = 0;
|
||||
|
||||
/** Return path to main database file for logs and error messages. */
|
||||
virtual std::string Filename() = 0;
|
||||
|
||||
virtual std::string Format() = 0;
|
||||
|
||||
std::atomic<unsigned int> nUpdateCounter;
|
||||
unsigned int nLastSeen;
|
||||
unsigned int nLastFlushed;
|
||||
int64_t nLastWalletUpdate;
|
||||
|
||||
/** Make a DatabaseBatch connected to this database */
|
||||
virtual std::unique_ptr<DatabaseBatch> MakeBatch(bool flush_on_close = true) = 0;
|
||||
};
|
||||
|
||||
/** RAII class that provides access to a DummyDatabase. Never fails. */
|
||||
class DummyBatch : public DatabaseBatch
|
||||
{
|
||||
private:
|
||||
bool ReadKey(CDataStream&& key, CDataStream& value) override { return true; }
|
||||
bool WriteKey(CDataStream&& key, CDataStream&& value, bool overwrite=true) override { return true; }
|
||||
bool EraseKey(CDataStream&& key) override { return true; }
|
||||
bool HasKey(CDataStream&& key) override { return true; }
|
||||
|
||||
public:
|
||||
void Flush() override {}
|
||||
void Close() override {}
|
||||
|
||||
bool StartCursor() override { return true; }
|
||||
bool ReadAtCursor(CDataStream& ssKey, CDataStream& ssValue, bool& complete) override { return true; }
|
||||
void CloseCursor() override {}
|
||||
bool TxnBegin() override { return true; }
|
||||
bool TxnCommit() override { return true; }
|
||||
bool TxnAbort() override { return true; }
|
||||
};
|
||||
|
||||
/** A dummy WalletDatabase that does nothing and never fails. Only used by unit tests.
|
||||
**/
|
||||
class DummyDatabase : public WalletDatabase
|
||||
{
|
||||
public:
|
||||
void Open() override {};
|
||||
void AddRef() override {}
|
||||
void RemoveRef() override {}
|
||||
bool Rewrite(const char* pszSkip=nullptr) override { return true; }
|
||||
bool Backup(const std::string& strDest) const override { return true; }
|
||||
void Close() override {}
|
||||
void Flush() override {}
|
||||
bool PeriodicFlush() override { return true; }
|
||||
void IncrementUpdateCounter() override { ++nUpdateCounter; }
|
||||
void ReloadDbEnv() override {}
|
||||
std::string Filename() override { return "dummy"; }
|
||||
std::string Format() override { return "dummy"; }
|
||||
std::unique_ptr<DatabaseBatch> MakeBatch(bool flush_on_close = true) override { return std::make_unique<DummyBatch>(); }
|
||||
};
|
||||
|
||||
enum class DatabaseFormat {
|
||||
BERKELEY,
|
||||
SQLITE,
|
||||
};
|
||||
|
||||
struct DatabaseOptions {
|
||||
bool require_existing = false;
|
||||
bool require_create = false;
|
||||
std::optional<DatabaseFormat> require_format;
|
||||
uint64_t create_flags = 0;
|
||||
SecureString create_passphrase;
|
||||
bool verify = true;
|
||||
};
|
||||
|
||||
enum class DatabaseStatus {
|
||||
SUCCESS,
|
||||
FAILED_BAD_PATH,
|
||||
FAILED_BAD_FORMAT,
|
||||
FAILED_ALREADY_LOADED,
|
||||
FAILED_ALREADY_EXISTS,
|
||||
FAILED_NOT_FOUND,
|
||||
FAILED_CREATE,
|
||||
FAILED_LOAD,
|
||||
FAILED_VERIFY,
|
||||
FAILED_ENCRYPT,
|
||||
};
|
||||
|
||||
/** Recursively list database paths in directory. */
|
||||
std::vector<fs::path> ListDatabases(const fs::path& path);
|
||||
|
||||
std::unique_ptr<WalletDatabase> MakeDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error);
|
||||
|
||||
fs::path BDBDataFile(const fs::path& path);
|
||||
fs::path SQLiteDataFile(const fs::path& path);
|
||||
bool IsBDBFile(const fs::path& path);
|
||||
bool IsSQLiteFile(const fs::path& path);
|
||||
|
||||
#endif // DIGIBYTE_WALLET_DB_H
|
||||
281
src/wallet/dump.cpp
Normal file
281
src/wallet/dump.cpp
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
// Copyright (c) 2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <wallet/dump.h>
|
||||
|
||||
#include <util/translation.h>
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
static const std::string DUMP_MAGIC = "DIGIBYTE_CORE_WALLET_DUMP";
|
||||
uint32_t DUMP_VERSION = 1;
|
||||
|
||||
bool DumpWallet(CWallet& wallet, bilingual_str& error)
|
||||
{
|
||||
// Get the dumpfile
|
||||
std::string dump_filename = gArgs.GetArg("-dumpfile", "");
|
||||
if (dump_filename.empty()) {
|
||||
error = _("No dump file provided. To use dump, -dumpfile=<filename> must be provided.");
|
||||
return false;
|
||||
}
|
||||
|
||||
fs::path path = dump_filename;
|
||||
path = fs::absolute(path);
|
||||
if (fs::exists(path)) {
|
||||
error = strprintf(_("File %s already exists. If you are sure this is what you want, move it out of the way first."), path.string());
|
||||
return false;
|
||||
}
|
||||
fsbridge::ofstream dump_file;
|
||||
dump_file.open(path);
|
||||
if (dump_file.fail()) {
|
||||
error = strprintf(_("Unable to open %s for writing"), path.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
CHashWriter hasher(0, 0);
|
||||
|
||||
WalletDatabase& db = wallet.GetDatabase();
|
||||
std::unique_ptr<DatabaseBatch> batch = db.MakeBatch();
|
||||
|
||||
bool ret = true;
|
||||
if (!batch->StartCursor()) {
|
||||
error = _("Error: Couldn't create cursor into database");
|
||||
ret = false;
|
||||
}
|
||||
|
||||
// Write out a magic string with version
|
||||
std::string line = strprintf("%s,%u\n", DUMP_MAGIC, DUMP_VERSION);
|
||||
dump_file.write(line.data(), line.size());
|
||||
hasher.write(line.data(), line.size());
|
||||
|
||||
// Write out the file format
|
||||
line = strprintf("%s,%s\n", "format", db.Format());
|
||||
dump_file.write(line.data(), line.size());
|
||||
hasher.write(line.data(), line.size());
|
||||
|
||||
if (ret) {
|
||||
|
||||
// Read the records
|
||||
while (true) {
|
||||
CDataStream ss_key(SER_DISK, CLIENT_VERSION);
|
||||
CDataStream ss_value(SER_DISK, CLIENT_VERSION);
|
||||
bool complete;
|
||||
ret = batch->ReadAtCursor(ss_key, ss_value, complete);
|
||||
if (complete) {
|
||||
ret = true;
|
||||
break;
|
||||
} else if (!ret) {
|
||||
error = _("Error reading next record from wallet database");
|
||||
break;
|
||||
}
|
||||
std::string key_str = HexStr(ss_key);
|
||||
std::string value_str = HexStr(ss_value);
|
||||
line = strprintf("%s,%s\n", key_str, value_str);
|
||||
dump_file.write(line.data(), line.size());
|
||||
hasher.write(line.data(), line.size());
|
||||
}
|
||||
}
|
||||
|
||||
batch->CloseCursor();
|
||||
batch.reset();
|
||||
|
||||
// Close the wallet after we're done with it. The caller won't be doing this
|
||||
wallet.Close();
|
||||
|
||||
if (ret) {
|
||||
// Write the hash
|
||||
tfm::format(dump_file, "checksum,%s\n", HexStr(hasher.GetHash()));
|
||||
dump_file.close();
|
||||
} else {
|
||||
// Remove the dumpfile on failure
|
||||
dump_file.close();
|
||||
fs::remove(path);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// The standard wallet deleter function blocks on the validation interface
|
||||
// queue, which doesn't exist for the digibyte-wallet. Define our own
|
||||
// deleter here.
|
||||
static void WalletToolReleaseWallet(CWallet* wallet)
|
||||
{
|
||||
wallet->WalletLogPrintf("Releasing wallet\n");
|
||||
wallet->Close();
|
||||
delete wallet;
|
||||
}
|
||||
|
||||
bool CreateFromDump(const std::string& name, const fs::path& wallet_path, bilingual_str& error, std::vector<bilingual_str>& warnings)
|
||||
{
|
||||
// Get the dumpfile
|
||||
std::string dump_filename = gArgs.GetArg("-dumpfile", "");
|
||||
if (dump_filename.empty()) {
|
||||
error = _("No dump file provided. To use createfromdump, -dumpfile=<filename> must be provided.");
|
||||
return false;
|
||||
}
|
||||
|
||||
fs::path dump_path = dump_filename;
|
||||
dump_path = fs::absolute(dump_path);
|
||||
if (!fs::exists(dump_path)) {
|
||||
error = strprintf(_("Dump file %s does not exist."), dump_path.string());
|
||||
return false;
|
||||
}
|
||||
fsbridge::ifstream dump_file(dump_path);
|
||||
|
||||
// Compute the checksum
|
||||
CHashWriter hasher(0, 0);
|
||||
uint256 checksum;
|
||||
|
||||
// Check the magic and version
|
||||
std::string magic_key;
|
||||
std::getline(dump_file, magic_key, ',');
|
||||
std::string version_value;
|
||||
std::getline(dump_file, version_value, '\n');
|
||||
if (magic_key != DUMP_MAGIC) {
|
||||
error = strprintf(_("Error: Dumpfile identifier record is incorrect. Got \"%s\", expected \"%s\"."), magic_key, DUMP_MAGIC);
|
||||
dump_file.close();
|
||||
return false;
|
||||
}
|
||||
// Check the version number (value of first record)
|
||||
uint32_t ver;
|
||||
if (!ParseUInt32(version_value, &ver)) {
|
||||
error =strprintf(_("Error: Unable to parse version %u as a uint32_t"), version_value);
|
||||
dump_file.close();
|
||||
return false;
|
||||
}
|
||||
if (ver != DUMP_VERSION) {
|
||||
error = strprintf(_("Error: Dumpfile version is not supported. This version of digibyte-wallet only supports version 1 dumpfiles. Got dumpfile with version %s"), version_value);
|
||||
dump_file.close();
|
||||
return false;
|
||||
}
|
||||
std::string magic_hasher_line = strprintf("%s,%s\n", magic_key, version_value);
|
||||
hasher.write(magic_hasher_line.data(), magic_hasher_line.size());
|
||||
|
||||
// Get the stored file format
|
||||
std::string format_key;
|
||||
std::getline(dump_file, format_key, ',');
|
||||
std::string format_value;
|
||||
std::getline(dump_file, format_value, '\n');
|
||||
if (format_key != "format") {
|
||||
error = strprintf(_("Error: Dumpfile format record is incorrect. Got \"%s\", expected \"format\"."), format_key);
|
||||
dump_file.close();
|
||||
return false;
|
||||
}
|
||||
// Get the data file format with format_value as the default
|
||||
std::string file_format = gArgs.GetArg("-format", format_value);
|
||||
if (file_format.empty()) {
|
||||
error = _("No wallet file format provided. To use createfromdump, -format=<format> must be provided.");
|
||||
return false;
|
||||
}
|
||||
DatabaseFormat data_format;
|
||||
if (file_format == "bdb") {
|
||||
data_format = DatabaseFormat::BERKELEY;
|
||||
} else if (file_format == "sqlite") {
|
||||
data_format = DatabaseFormat::SQLITE;
|
||||
} else {
|
||||
error = strprintf(_("Unknown wallet file format \"%s\" provided. Please provide one of \"bdb\" or \"sqlite\"."), file_format);
|
||||
return false;
|
||||
}
|
||||
if (file_format != format_value) {
|
||||
warnings.push_back(strprintf(_("Warning: Dumpfile wallet format \"%s\" does not match command line specified format \"%s\"."), format_value, file_format));
|
||||
}
|
||||
std::string format_hasher_line = strprintf("%s,%s\n", format_key, format_value);
|
||||
hasher.write(format_hasher_line.data(), format_hasher_line.size());
|
||||
|
||||
DatabaseOptions options;
|
||||
DatabaseStatus status;
|
||||
options.require_create = true;
|
||||
options.require_format = data_format;
|
||||
std::unique_ptr<WalletDatabase> database = MakeDatabase(wallet_path, options, status, error);
|
||||
if (!database) return false;
|
||||
|
||||
// dummy chain interface
|
||||
bool ret = true;
|
||||
std::shared_ptr<CWallet> wallet(new CWallet(nullptr /* chain */, name, std::move(database)), WalletToolReleaseWallet);
|
||||
{
|
||||
LOCK(wallet->cs_wallet);
|
||||
DBErrors load_wallet_ret = wallet->LoadWallet();
|
||||
if (load_wallet_ret != DBErrors::LOAD_OK) {
|
||||
error = strprintf(_("Error creating %s"), name);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the database handle
|
||||
WalletDatabase& db = wallet->GetDatabase();
|
||||
std::unique_ptr<DatabaseBatch> batch = db.MakeBatch();
|
||||
batch->TxnBegin();
|
||||
|
||||
// Read the records from the dump file and write them to the database
|
||||
while (dump_file.good()) {
|
||||
std::string key;
|
||||
std::getline(dump_file, key, ',');
|
||||
std::string value;
|
||||
std::getline(dump_file, value, '\n');
|
||||
|
||||
if (key == "checksum") {
|
||||
std::vector<unsigned char> parsed_checksum = ParseHex(value);
|
||||
std::copy(parsed_checksum.begin(), parsed_checksum.end(), checksum.begin());
|
||||
break;
|
||||
}
|
||||
|
||||
std::string line = strprintf("%s,%s\n", key, value);
|
||||
hasher.write(line.data(), line.size());
|
||||
|
||||
if (key.empty() || value.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsHex(key)) {
|
||||
error = strprintf(_("Error: Got key that was not hex: %s"), key);
|
||||
ret = false;
|
||||
break;
|
||||
}
|
||||
if (!IsHex(value)) {
|
||||
error = strprintf(_("Error: Got value that was not hex: %s"), value);
|
||||
ret = false;
|
||||
break;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> k = ParseHex(key);
|
||||
std::vector<unsigned char> v = ParseHex(value);
|
||||
|
||||
CDataStream ss_key(k, SER_DISK, CLIENT_VERSION);
|
||||
CDataStream ss_value(v, SER_DISK, CLIENT_VERSION);
|
||||
|
||||
if (!batch->Write(ss_key, ss_value)) {
|
||||
error = strprintf(_("Error: Unable to write record to new wallet"));
|
||||
ret = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ret) {
|
||||
uint256 comp_checksum = hasher.GetHash();
|
||||
if (checksum.IsNull()) {
|
||||
error = _("Error: Missing checksum");
|
||||
ret = false;
|
||||
} else if (checksum != comp_checksum) {
|
||||
error = strprintf(_("Error: Dumpfile checksum does not match. Computed %s, expected %s"), HexStr(comp_checksum), HexStr(checksum));
|
||||
ret = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ret) {
|
||||
batch->TxnCommit();
|
||||
} else {
|
||||
batch->TxnAbort();
|
||||
}
|
||||
|
||||
batch.reset();
|
||||
|
||||
dump_file.close();
|
||||
}
|
||||
wallet.reset(); // The pointer deleter will close the wallet for us.
|
||||
|
||||
// Remove the wallet dir if we have a failure
|
||||
if (!ret) {
|
||||
fs::remove_all(wallet_path);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
17
src/wallet/dump.h
Normal file
17
src/wallet/dump.h
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) 2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_DUMP_H
|
||||
#define DIGIBYTE_WALLET_DUMP_H
|
||||
|
||||
#include <fs.h>
|
||||
|
||||
class CWallet;
|
||||
|
||||
struct bilingual_str;
|
||||
|
||||
bool DumpWallet(CWallet& wallet, bilingual_str& error);
|
||||
bool CreateFromDump(const std::string& name, const fs::path& wallet_path, bilingual_str& error, std::vector<bilingual_str>& warnings);
|
||||
|
||||
#endif // DIGIBYTE_WALLET_DUMP_H
|
||||
84
src/wallet/external_signer_scriptpubkeyman.cpp
Normal file
84
src/wallet/external_signer_scriptpubkeyman.cpp
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright (c) 2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <chainparams.h>
|
||||
#include <external_signer.h>
|
||||
#include <wallet/external_signer_scriptpubkeyman.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
bool ExternalSignerScriptPubKeyMan::SetupDescriptor(std::unique_ptr<Descriptor> desc)
|
||||
{
|
||||
LOCK(cs_desc_man);
|
||||
assert(m_storage.IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
|
||||
assert(m_storage.IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER));
|
||||
|
||||
int64_t creation_time = GetTime();
|
||||
|
||||
// Make the descriptor
|
||||
WalletDescriptor w_desc(std::move(desc), creation_time, 0, 0, 0);
|
||||
m_wallet_descriptor = w_desc;
|
||||
|
||||
// Store the descriptor
|
||||
WalletBatch batch(m_storage.GetDatabase());
|
||||
if (!batch.WriteDescriptor(GetID(), m_wallet_descriptor)) {
|
||||
throw std::runtime_error(std::string(__func__) + ": writing descriptor failed");
|
||||
}
|
||||
|
||||
// TopUp
|
||||
TopUp();
|
||||
|
||||
m_storage.UnsetBlankWalletFlag(batch);
|
||||
return true;
|
||||
}
|
||||
|
||||
ExternalSigner ExternalSignerScriptPubKeyMan::GetExternalSigner() {
|
||||
const std::string command = gArgs.GetArg("-signer", "");
|
||||
if (command == "") throw std::runtime_error(std::string(__func__) + ": restart vextad with -signer=<cmd>");
|
||||
std::vector<ExternalSigner> signers;
|
||||
ExternalSigner::Enumerate(command, signers, Params().NetworkIDString());
|
||||
if (signers.empty()) throw std::runtime_error(std::string(__func__) + ": No external signers found");
|
||||
// TODO: add fingerprint argument in case of multiple signers
|
||||
return signers[0];
|
||||
}
|
||||
|
||||
bool ExternalSignerScriptPubKeyMan::DisplayAddress(const CScript scriptPubKey, const ExternalSigner &signer) const
|
||||
{
|
||||
// TODO: avoid the need to infer a descriptor from inside a descriptor wallet
|
||||
auto provider = GetSolvingProvider(scriptPubKey);
|
||||
auto descriptor = InferDescriptor(scriptPubKey, *provider);
|
||||
|
||||
signer.DisplayAddress(descriptor->ToString());
|
||||
// TODO inspect result
|
||||
return true;
|
||||
}
|
||||
|
||||
// If sign is true, transaction must previously have been filled
|
||||
TransactionError ExternalSignerScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type, bool sign, bool bip32derivs, int* n_signed) const
|
||||
{
|
||||
if (!sign) {
|
||||
return DescriptorScriptPubKeyMan::FillPSBT(psbt, txdata, sighash_type, false, bip32derivs, n_signed);
|
||||
}
|
||||
|
||||
// Already complete if every input is now signed
|
||||
bool complete = true;
|
||||
for (const auto& input : psbt.inputs) {
|
||||
// TODO: for multisig wallets, we should only care if all _our_ inputs are signed
|
||||
complete &= PSBTInputSigned(input);
|
||||
}
|
||||
if (complete) return TransactionError::OK;
|
||||
|
||||
std::string strFailReason;
|
||||
if(!GetExternalSigner().SignTransaction(psbt, strFailReason)) {
|
||||
tfm::format(std::cerr, "Failed to sign: %s\n", strFailReason);
|
||||
return TransactionError::EXTERNAL_SIGNER_FAILED;
|
||||
}
|
||||
FinalizePSBT(psbt); // This won't work in a multisig setup
|
||||
return TransactionError::OK;
|
||||
}
|
||||
33
src/wallet/external_signer_scriptpubkeyman.h
Normal file
33
src/wallet/external_signer_scriptpubkeyman.h
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) 2019-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_EXTERNAL_SIGNER_SCRIPTPUBKEYMAN_H
|
||||
#define DIGIBYTE_WALLET_EXTERNAL_SIGNER_SCRIPTPUBKEYMAN_H
|
||||
|
||||
#include <wallet/scriptpubkeyman.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class ExternalSignerScriptPubKeyMan : public DescriptorScriptPubKeyMan
|
||||
{
|
||||
public:
|
||||
ExternalSignerScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor)
|
||||
: DescriptorScriptPubKeyMan(storage, descriptor)
|
||||
{}
|
||||
ExternalSignerScriptPubKeyMan(WalletStorage& storage)
|
||||
: DescriptorScriptPubKeyMan(storage)
|
||||
{}
|
||||
|
||||
/** Provide a descriptor at setup time
|
||||
* Returns false if already setup or setup fails, true if setup is successful
|
||||
*/
|
||||
bool SetupDescriptor(std::unique_ptr<Descriptor>desc);
|
||||
|
||||
static ExternalSigner GetExternalSigner();
|
||||
|
||||
bool DisplayAddress(const CScript scriptPubKey, const ExternalSigner &signer) const;
|
||||
|
||||
TransactionError FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override;
|
||||
};
|
||||
#endif // DIGIBYTE_WALLET_EXTERNAL_SIGNER_SCRIPTPUBKEYMAN_H
|
||||
287
src/wallet/feebumper.cpp
Normal file
287
src/wallet/feebumper.cpp
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
// Copyright (c) 2017-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2017-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <interfaces/chain.h>
|
||||
#include <policy/fees.h>
|
||||
#include <policy/policy.h>
|
||||
#include <util/moneystr.h>
|
||||
#include <util/rbf.h>
|
||||
#include <util/system.h>
|
||||
#include <util/translation.h>
|
||||
#include <wallet/coincontrol.h>
|
||||
#include <wallet/feebumper.h>
|
||||
#include <wallet/fees.h>
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
//! Check whether transaction has descendant in wallet or mempool, or has been
|
||||
//! mined, or conflicts with a mined transaction. Return a feebumper::Result.
|
||||
static feebumper::Result PreconditionChecks(const CWallet& wallet, const CWalletTx& wtx, std::vector<bilingual_str>& errors) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
|
||||
{
|
||||
if (wallet.HasWalletSpend(wtx.GetHash())) {
|
||||
errors.push_back(Untranslated("Transaction has descendants in the wallet"));
|
||||
return feebumper::Result::INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
{
|
||||
if (wallet.chain().hasDescendantsInMempool(wtx.GetHash())) {
|
||||
errors.push_back(Untranslated("Transaction has descendants in the mempool"));
|
||||
return feebumper::Result::INVALID_PARAMETER;
|
||||
}
|
||||
}
|
||||
|
||||
if (wtx.GetDepthInMainChain() != 0) {
|
||||
errors.push_back(Untranslated("Transaction has been mined, or is conflicted with a mined transaction"));
|
||||
return feebumper::Result::WALLET_ERROR;
|
||||
}
|
||||
|
||||
if (!SignalsOptInRBF(*wtx.tx)) {
|
||||
errors.push_back(Untranslated("Transaction is not BIP 125 replaceable"));
|
||||
return feebumper::Result::WALLET_ERROR;
|
||||
}
|
||||
|
||||
if (wtx.mapValue.count("replaced_by_txid")) {
|
||||
errors.push_back(strprintf(Untranslated("Cannot bump transaction %s which was already bumped by transaction %s"), wtx.GetHash().ToString(), wtx.mapValue.at("replaced_by_txid")));
|
||||
return feebumper::Result::WALLET_ERROR;
|
||||
}
|
||||
|
||||
// check that original tx consists entirely of our inputs
|
||||
// if not, we can't bump the fee, because the wallet has no way of knowing the value of the other inputs (thus the fee)
|
||||
isminefilter filter = wallet.GetLegacyScriptPubKeyMan() && wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) ? ISMINE_WATCH_ONLY : ISMINE_SPENDABLE;
|
||||
if (!wallet.IsAllFromMe(*wtx.tx, filter)) {
|
||||
errors.push_back(Untranslated("Transaction contains inputs that don't belong to this wallet"));
|
||||
return feebumper::Result::WALLET_ERROR;
|
||||
}
|
||||
|
||||
|
||||
return feebumper::Result::OK;
|
||||
}
|
||||
|
||||
//! Check if the user provided a valid feeRate
|
||||
static feebumper::Result CheckFeeRate(const CWallet& wallet, const CWalletTx& wtx, const CFeeRate& newFeerate, const int64_t maxTxSize, std::vector<bilingual_str>& errors)
|
||||
{
|
||||
// check that fee rate is higher than mempool's minimum fee
|
||||
// (no point in bumping fee if we know that the new tx won't be accepted to the mempool)
|
||||
// This may occur if the user set fee_rate or paytxfee too low, if fallbackfee is too low, or, perhaps,
|
||||
// in a rare situation where the mempool minimum fee increased significantly since the fee estimation just a
|
||||
// moment earlier. In this case, we report an error to the user, who may adjust the fee.
|
||||
CFeeRate minMempoolFeeRate = wallet.chain().mempoolMinFee();
|
||||
|
||||
if (newFeerate.GetFeePerK() < minMempoolFeeRate.GetFeePerK()) {
|
||||
errors.push_back(strprintf(
|
||||
Untranslated("New fee rate (%s) is lower than the minimum fee rate (%s) to get into the mempool -- "),
|
||||
FormatMoney(newFeerate.GetFeePerK()),
|
||||
FormatMoney(minMempoolFeeRate.GetFeePerK())));
|
||||
return feebumper::Result::WALLET_ERROR;
|
||||
}
|
||||
|
||||
CAmount new_total_fee = newFeerate.GetFee(maxTxSize);
|
||||
|
||||
CFeeRate incrementalRelayFee = std::max(wallet.chain().relayIncrementalFee(), CFeeRate(WALLET_INCREMENTAL_RELAY_FEE));
|
||||
|
||||
// Given old total fee and transaction size, calculate the old feeRate
|
||||
isminefilter filter = wallet.GetLegacyScriptPubKeyMan() && wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) ? ISMINE_WATCH_ONLY : ISMINE_SPENDABLE;
|
||||
CAmount old_fee = wtx.GetDebit(filter) - wtx.tx->GetValueOut();
|
||||
const int64_t txSize = GetVirtualTransactionSize(*(wtx.tx));
|
||||
CFeeRate nOldFeeRate(old_fee, txSize);
|
||||
// Min total fee is old fee + relay fee
|
||||
CAmount minTotalFee = nOldFeeRate.GetFee(maxTxSize) + incrementalRelayFee.GetFee(maxTxSize);
|
||||
|
||||
if (new_total_fee < minTotalFee) {
|
||||
errors.push_back(strprintf(Untranslated("Insufficient total fee %s, must be at least %s (oldFee %s + incrementalFee %s)"),
|
||||
FormatMoney(new_total_fee), FormatMoney(minTotalFee), FormatMoney(nOldFeeRate.GetFee(maxTxSize)), FormatMoney(incrementalRelayFee.GetFee(maxTxSize))));
|
||||
return feebumper::Result::INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
CAmount requiredFee = GetRequiredFee(wallet, maxTxSize);
|
||||
if (new_total_fee < requiredFee) {
|
||||
errors.push_back(strprintf(Untranslated("Insufficient total fee (cannot be less than required fee %s)"),
|
||||
FormatMoney(requiredFee)));
|
||||
return feebumper::Result::INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
// Check that in all cases the new fee doesn't violate maxTxFee
|
||||
const CAmount max_tx_fee = wallet.m_default_max_tx_fee;
|
||||
if (new_total_fee > max_tx_fee) {
|
||||
errors.push_back(strprintf(Untranslated("Specified or calculated fee %s is too high (cannot be higher than -maxtxfee %s)"),
|
||||
FormatMoney(new_total_fee), FormatMoney(max_tx_fee)));
|
||||
return feebumper::Result::WALLET_ERROR;
|
||||
}
|
||||
|
||||
return feebumper::Result::OK;
|
||||
}
|
||||
|
||||
static CFeeRate EstimateFeeRate(const CWallet& wallet, const CWalletTx& wtx, const CAmount old_fee, const CCoinControl& coin_control)
|
||||
{
|
||||
// Get the fee rate of the original transaction. This is calculated from
|
||||
// the tx fee/vsize, so it may have been rounded down. Add 1 satoshi to the
|
||||
// result.
|
||||
int64_t txSize = GetVirtualTransactionSize(*(wtx.tx));
|
||||
CFeeRate feerate(old_fee, txSize);
|
||||
feerate += CFeeRate(1);
|
||||
|
||||
// The node has a configurable incremental relay fee. Increment the fee by
|
||||
// the minimum of that and the wallet's conservative
|
||||
// WALLET_INCREMENTAL_RELAY_FEE value to future proof against changes to
|
||||
// network wide policy for incremental relay fee that our node may not be
|
||||
// aware of. This ensures we're over the required relay fee rate
|
||||
// (BIP 125 rule 4). The replacement tx will be at least as large as the
|
||||
// original tx, so the total fee will be greater (BIP 125 rule 3)
|
||||
CFeeRate node_incremental_relay_fee = wallet.chain().relayIncrementalFee();
|
||||
CFeeRate wallet_incremental_relay_fee = CFeeRate(WALLET_INCREMENTAL_RELAY_FEE);
|
||||
feerate += std::max(node_incremental_relay_fee, wallet_incremental_relay_fee);
|
||||
|
||||
// Fee rate must also be at least the wallet's GetMinimumFeeRate
|
||||
CFeeRate min_feerate(GetMinimumFeeRate(wallet, coin_control, /* feeCalc */ nullptr));
|
||||
|
||||
// Set the required fee rate for the replacement transaction in coin control.
|
||||
return std::max(feerate, min_feerate);
|
||||
}
|
||||
|
||||
namespace feebumper {
|
||||
|
||||
bool TransactionCanBeBumped(const CWallet& wallet, const uint256& txid)
|
||||
{
|
||||
LOCK(wallet.cs_wallet);
|
||||
const CWalletTx* wtx = wallet.GetWalletTx(txid);
|
||||
if (wtx == nullptr) return false;
|
||||
|
||||
std::vector<bilingual_str> errors_dummy;
|
||||
feebumper::Result res = PreconditionChecks(wallet, *wtx, errors_dummy);
|
||||
return res == feebumper::Result::OK;
|
||||
}
|
||||
|
||||
Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCoinControl& coin_control, std::vector<bilingual_str>& errors,
|
||||
CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx)
|
||||
{
|
||||
// We are going to modify coin control later, copy to re-use
|
||||
CCoinControl new_coin_control(coin_control);
|
||||
|
||||
LOCK(wallet.cs_wallet);
|
||||
errors.clear();
|
||||
auto it = wallet.mapWallet.find(txid);
|
||||
if (it == wallet.mapWallet.end()) {
|
||||
errors.push_back(Untranslated("Invalid or non-wallet transaction id"));
|
||||
return Result::INVALID_ADDRESS_OR_KEY;
|
||||
}
|
||||
const CWalletTx& wtx = it->second;
|
||||
|
||||
Result result = PreconditionChecks(wallet, wtx, errors);
|
||||
if (result != Result::OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fill in recipients(and preserve a single change key if there is one)
|
||||
std::vector<CRecipient> recipients;
|
||||
for (const auto& output : wtx.tx->vout) {
|
||||
if (!wallet.IsChange(output)) {
|
||||
CRecipient recipient = {output.scriptPubKey, output.nValue, false};
|
||||
recipients.push_back(recipient);
|
||||
} else {
|
||||
CTxDestination change_dest;
|
||||
ExtractDestination(output.scriptPubKey, change_dest);
|
||||
new_coin_control.destChange = change_dest;
|
||||
}
|
||||
}
|
||||
|
||||
isminefilter filter = wallet.GetLegacyScriptPubKeyMan() && wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) ? ISMINE_WATCH_ONLY : ISMINE_SPENDABLE;
|
||||
old_fee = wtx.GetDebit(filter) - wtx.tx->GetValueOut();
|
||||
|
||||
if (coin_control.m_feerate) {
|
||||
// The user provided a feeRate argument.
|
||||
// We calculate this here to avoid compiler warning on the cs_wallet lock
|
||||
const int64_t maxTxSize{CalculateMaximumSignedTxSize(*wtx.tx, &wallet).vsize};
|
||||
Result res = CheckFeeRate(wallet, wtx, *new_coin_control.m_feerate, maxTxSize, errors);
|
||||
if (res != Result::OK) {
|
||||
return res;
|
||||
}
|
||||
} else {
|
||||
// The user did not provide a feeRate argument
|
||||
new_coin_control.m_feerate = EstimateFeeRate(wallet, wtx, old_fee, new_coin_control);
|
||||
}
|
||||
|
||||
// Fill in required inputs we are double-spending(all of them)
|
||||
// N.B.: bip125 doesn't require all the inputs in the replaced transaction to be
|
||||
// used in the replacement transaction, but it's very important for wallets to make
|
||||
// sure that happens. If not, it would be possible to bump a transaction A twice to
|
||||
// A2 and A3 where A2 and A3 don't conflict (or alternatively bump A to A2 and A2
|
||||
// to A3 where A and A3 don't conflict). If both later get confirmed then the sender
|
||||
// has accidentally double paid.
|
||||
for (const auto& inputs : wtx.tx->vin) {
|
||||
new_coin_control.Select(COutPoint(inputs.prevout));
|
||||
}
|
||||
new_coin_control.fAllowOtherInputs = true;
|
||||
|
||||
// We cannot source new unconfirmed inputs(bip125 rule 2)
|
||||
new_coin_control.m_min_depth = 1;
|
||||
|
||||
CTransactionRef tx_new;
|
||||
CAmount fee_ret;
|
||||
int change_pos_in_out = -1; // No requested location for change
|
||||
bilingual_str fail_reason;
|
||||
FeeCalculation fee_calc_out;
|
||||
if (!wallet.CreateTransaction(recipients, tx_new, fee_ret, change_pos_in_out, fail_reason, new_coin_control, fee_calc_out, false)) {
|
||||
errors.push_back(Untranslated("Unable to create transaction.") + Untranslated(" ") + fail_reason);
|
||||
return Result::WALLET_ERROR;
|
||||
}
|
||||
|
||||
// Write back new fee if successful
|
||||
new_fee = fee_ret;
|
||||
|
||||
// Write back transaction
|
||||
mtx = CMutableTransaction(*tx_new);
|
||||
// Mark new tx not replaceable, if requested.
|
||||
if (!coin_control.m_signal_bip125_rbf.value_or(wallet.m_signal_rbf)) {
|
||||
for (auto& input : mtx.vin) {
|
||||
if (input.nSequence < 0xfffffffe) input.nSequence = 0xfffffffe;
|
||||
}
|
||||
}
|
||||
|
||||
return Result::OK;
|
||||
}
|
||||
|
||||
bool SignTransaction(CWallet& wallet, CMutableTransaction& mtx) {
|
||||
LOCK(wallet.cs_wallet);
|
||||
return wallet.SignTransaction(mtx);
|
||||
}
|
||||
|
||||
Result CommitTransaction(CWallet& wallet, const uint256& txid, CMutableTransaction&& mtx, std::vector<bilingual_str>& errors, uint256& bumped_txid)
|
||||
{
|
||||
LOCK(wallet.cs_wallet);
|
||||
if (!errors.empty()) {
|
||||
return Result::MISC_ERROR;
|
||||
}
|
||||
auto it = txid.IsNull() ? wallet.mapWallet.end() : wallet.mapWallet.find(txid);
|
||||
if (it == wallet.mapWallet.end()) {
|
||||
errors.push_back(Untranslated("Invalid or non-wallet transaction id"));
|
||||
return Result::MISC_ERROR;
|
||||
}
|
||||
const CWalletTx& oldWtx = it->second;
|
||||
|
||||
// make sure the transaction still has no descendants and hasn't been mined in the meantime
|
||||
Result result = PreconditionChecks(wallet, oldWtx, errors);
|
||||
if (result != Result::OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// commit/broadcast the tx
|
||||
CTransactionRef tx = MakeTransactionRef(std::move(mtx));
|
||||
mapValue_t mapValue = oldWtx.mapValue;
|
||||
mapValue["replaces_txid"] = oldWtx.GetHash().ToString();
|
||||
|
||||
wallet.CommitTransaction(tx, std::move(mapValue), oldWtx.vOrderForm);
|
||||
|
||||
// mark the original tx as bumped
|
||||
bumped_txid = tx->GetHash();
|
||||
if (!wallet.MarkReplaced(oldWtx.GetHash(), bumped_txid)) {
|
||||
// TODO: see if JSON-RPC has a standard way of returning a response
|
||||
// along with an exception. It would be good to return information about
|
||||
// wtxBumped to the caller even if marking the original transaction
|
||||
// replaced does not succeed for some reason.
|
||||
errors.push_back(Untranslated("Created new bumpfee transaction but could not mark the original transaction as replaced"));
|
||||
}
|
||||
return Result::OK;
|
||||
}
|
||||
|
||||
} // namespace feebumper
|
||||
59
src/wallet/feebumper.h
Normal file
59
src/wallet/feebumper.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) 2017-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2017-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_FEEBUMPER_H
|
||||
#define DIGIBYTE_WALLET_FEEBUMPER_H
|
||||
|
||||
#include <primitives/transaction.h>
|
||||
|
||||
class CWallet;
|
||||
class CWalletTx;
|
||||
class uint256;
|
||||
class CCoinControl;
|
||||
enum class FeeEstimateMode;
|
||||
struct bilingual_str;
|
||||
|
||||
namespace feebumper {
|
||||
|
||||
enum class Result
|
||||
{
|
||||
OK,
|
||||
INVALID_ADDRESS_OR_KEY,
|
||||
INVALID_REQUEST,
|
||||
INVALID_PARAMETER,
|
||||
WALLET_ERROR,
|
||||
MISC_ERROR,
|
||||
};
|
||||
|
||||
//! Return whether transaction can be bumped.
|
||||
bool TransactionCanBeBumped(const CWallet& wallet, const uint256& txid);
|
||||
|
||||
//! Create bumpfee transaction based on feerate estimates.
|
||||
Result CreateRateBumpTransaction(CWallet& wallet,
|
||||
const uint256& txid,
|
||||
const CCoinControl& coin_control,
|
||||
std::vector<bilingual_str>& errors,
|
||||
CAmount& old_fee,
|
||||
CAmount& new_fee,
|
||||
CMutableTransaction& mtx);
|
||||
|
||||
//! Sign the new transaction,
|
||||
//! @return false if the tx couldn't be found or if it was
|
||||
//! impossible to create the signature(s)
|
||||
bool SignTransaction(CWallet& wallet, CMutableTransaction& mtx);
|
||||
|
||||
//! Commit the bumpfee transaction.
|
||||
//! @return success in case of CWallet::CommitTransaction was successful,
|
||||
//! but sets errors if the tx could not be added to the mempool (will try later)
|
||||
//! or if the old transaction could not be marked as replaced.
|
||||
Result CommitTransaction(CWallet& wallet,
|
||||
const uint256& txid,
|
||||
CMutableTransaction&& mtx,
|
||||
std::vector<bilingual_str>& errors,
|
||||
uint256& bumped_txid);
|
||||
|
||||
} // namespace feebumper
|
||||
|
||||
#endif // DIGIBYTE_WALLET_FEEBUMPER_H
|
||||
93
src/wallet/fees.cpp
Normal file
93
src/wallet/fees.cpp
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <wallet/fees.h>
|
||||
|
||||
#include <wallet/coincontrol.h>
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
|
||||
CAmount GetRequiredFee(const CWallet& wallet, unsigned int nTxBytes)
|
||||
{
|
||||
return GetRequiredFeeRate(wallet).GetFee(nTxBytes);
|
||||
}
|
||||
|
||||
|
||||
CAmount GetMinimumFee(const CWallet& wallet, unsigned int nTxBytes, const CCoinControl& coin_control, FeeCalculation* feeCalc)
|
||||
{
|
||||
return GetMinimumFeeRate(wallet, coin_control, feeCalc).GetFee(nTxBytes);
|
||||
}
|
||||
|
||||
CFeeRate GetRequiredFeeRate(const CWallet& wallet)
|
||||
{
|
||||
return std::max(wallet.m_min_fee, wallet.chain().relayMinFee());
|
||||
}
|
||||
|
||||
CFeeRate GetMinimumFeeRate(const CWallet& wallet, const CCoinControl& coin_control, FeeCalculation* feeCalc)
|
||||
{
|
||||
/* User control of how to calculate fee uses the following parameter precedence:
|
||||
1. coin_control.m_feerate
|
||||
2. coin_control.m_confirm_target
|
||||
3. m_pay_tx_fee (user-set member variable of wallet)
|
||||
4. m_confirm_target (user-set member variable of wallet)
|
||||
The first parameter that is set is used.
|
||||
*/
|
||||
CFeeRate feerate_needed;
|
||||
if (coin_control.m_feerate) { // 1.
|
||||
feerate_needed = *(coin_control.m_feerate);
|
||||
if (feeCalc) feeCalc->reason = FeeReason::PAYTXFEE;
|
||||
// Allow to override automatic min/max check over coin control instance
|
||||
if (coin_control.fOverrideFeeRate) return feerate_needed;
|
||||
}
|
||||
else if (!coin_control.m_confirm_target && wallet.m_pay_tx_fee != CFeeRate(0)) { // 3. TODO: remove magic value of 0 for wallet member m_pay_tx_fee
|
||||
feerate_needed = wallet.m_pay_tx_fee;
|
||||
if (feeCalc) feeCalc->reason = FeeReason::PAYTXFEE;
|
||||
}
|
||||
else { // 2. or 4.
|
||||
// We will use smart fee estimation
|
||||
unsigned int target = coin_control.m_confirm_target ? *coin_control.m_confirm_target : wallet.m_confirm_target;
|
||||
// By default estimates are economical iff we are signaling opt-in-RBF
|
||||
bool conservative_estimate = !coin_control.m_signal_bip125_rbf.value_or(wallet.m_signal_rbf);
|
||||
// Allow to override the default fee estimate mode over the CoinControl instance
|
||||
if (coin_control.m_fee_mode == FeeEstimateMode::CONSERVATIVE) conservative_estimate = true;
|
||||
else if (coin_control.m_fee_mode == FeeEstimateMode::ECONOMICAL) conservative_estimate = false;
|
||||
|
||||
feerate_needed = wallet.chain().estimateSmartFee(target, conservative_estimate, feeCalc);
|
||||
if (feerate_needed == CFeeRate(0)) {
|
||||
// if we don't have enough data for estimateSmartFee, then use fallback fee
|
||||
feerate_needed = wallet.m_fallback_fee;
|
||||
if (feeCalc) feeCalc->reason = FeeReason::FALLBACK;
|
||||
|
||||
// directly return if fallback fee is disabled (feerate 0 == disabled)
|
||||
if (wallet.m_fallback_fee == CFeeRate(0)) return feerate_needed;
|
||||
}
|
||||
// Obey mempool min fee when using smart fee estimation
|
||||
CFeeRate min_mempool_feerate = wallet.chain().mempoolMinFee();
|
||||
if (feerate_needed < min_mempool_feerate) {
|
||||
feerate_needed = min_mempool_feerate;
|
||||
if (feeCalc) feeCalc->reason = FeeReason::MEMPOOL_MIN;
|
||||
}
|
||||
}
|
||||
|
||||
// prevent user from paying a fee below the required fee rate
|
||||
CFeeRate required_feerate = GetRequiredFeeRate(wallet);
|
||||
if (required_feerate > feerate_needed) {
|
||||
feerate_needed = required_feerate;
|
||||
if (feeCalc) feeCalc->reason = FeeReason::REQUIRED;
|
||||
}
|
||||
return feerate_needed;
|
||||
}
|
||||
|
||||
CFeeRate GetDiscardRate(const CWallet& wallet)
|
||||
{
|
||||
unsigned int highest_target = wallet.chain().estimateMaxBlocks();
|
||||
CFeeRate discard_rate = wallet.chain().estimateSmartFee(highest_target, false /* conservative */);
|
||||
// Don't let discard_rate be greater than longest possible fee estimate if we get a valid fee estimate
|
||||
discard_rate = (discard_rate == CFeeRate(0)) ? wallet.m_discard_rate : std::min(discard_rate, wallet.m_discard_rate);
|
||||
// Discard rate must be at least dustRelayFee
|
||||
discard_rate = std::max(discard_rate, wallet.chain().relayDustFee());
|
||||
return discard_rate;
|
||||
}
|
||||
46
src/wallet/fees.h
Normal file
46
src/wallet/fees.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2018 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2018 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_FEES_H
|
||||
#define DIGIBYTE_WALLET_FEES_H
|
||||
|
||||
#include <amount.h>
|
||||
|
||||
class CCoinControl;
|
||||
class CFeeRate;
|
||||
class CWallet;
|
||||
struct FeeCalculation;
|
||||
|
||||
/**
|
||||
* Return the minimum required absolute fee for this size
|
||||
* based on the required fee rate
|
||||
*/
|
||||
CAmount GetRequiredFee(const CWallet& wallet, unsigned int nTxBytes);
|
||||
|
||||
/**
|
||||
* Estimate the minimum fee considering user set parameters
|
||||
* and the required fee
|
||||
*/
|
||||
CAmount GetMinimumFee(const CWallet& wallet, unsigned int nTxBytes, const CCoinControl& coin_control, FeeCalculation* feeCalc);
|
||||
|
||||
/**
|
||||
* Return the minimum required feerate taking into account the
|
||||
* minimum relay feerate and user set minimum transaction feerate
|
||||
*/
|
||||
CFeeRate GetRequiredFeeRate(const CWallet& wallet);
|
||||
|
||||
/**
|
||||
* Estimate the minimum fee rate considering user set parameters
|
||||
* and the required fee
|
||||
*/
|
||||
CFeeRate GetMinimumFeeRate(const CWallet& wallet, const CCoinControl& coin_control, FeeCalculation* feeCalc);
|
||||
|
||||
/**
|
||||
* Return the maximum feerate for discarding change.
|
||||
*/
|
||||
CFeeRate GetDiscardRate(const CWallet& wallet);
|
||||
|
||||
#endif // DIGIBYTE_WALLET_FEES_H
|
||||
137
src/wallet/init.cpp
Normal file
137
src/wallet/init.cpp
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <init.h>
|
||||
#include <interfaces/chain.h>
|
||||
#include <interfaces/wallet.h>
|
||||
#include <net.h>
|
||||
#include <node/context.h>
|
||||
#include <node/ui_interface.h>
|
||||
#include <outputtype.h>
|
||||
#include <univalue.h>
|
||||
#include <util/check.h>
|
||||
#include <util/moneystr.h>
|
||||
#include <util/system.h>
|
||||
#include <util/translation.h>
|
||||
#ifdef USE_BDB
|
||||
#include <wallet/bdb.h>
|
||||
#endif
|
||||
#include <wallet/coincontrol.h>
|
||||
#include <wallet/wallet.h>
|
||||
#include <walletinitinterface.h>
|
||||
|
||||
class WalletInit : public WalletInitInterface
|
||||
{
|
||||
public:
|
||||
//! Was the wallet component compiled in.
|
||||
bool HasWalletSupport() const override {return true;}
|
||||
|
||||
//! Return the wallets help message.
|
||||
void AddWalletOptions(ArgsManager& argsman) const override;
|
||||
|
||||
//! Wallets parameter interaction
|
||||
bool ParameterInteraction() const override;
|
||||
|
||||
//! Add wallets that should be opened to list of chain clients.
|
||||
void Construct(NodeContext& node) const override;
|
||||
};
|
||||
|
||||
const WalletInitInterface& g_wallet_init_interface = WalletInit();
|
||||
|
||||
void WalletInit::AddWalletOptions(ArgsManager& argsman) const
|
||||
{
|
||||
argsman.AddArg("-addresstype", strprintf("What type of addresses to use (\"legacy\", \"p2sh-segwit\", or \"bech32\", default: \"%s\")", FormatOutputType(DEFAULT_ADDRESS_TYPE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-avoidpartialspends", strprintf("Group outputs by address, selecting many (possibly all) or none, instead of selecting on a per-output basis. Privacy is improved as addresses are mostly swept with fewer transactions and outputs are aggregated in clean change addresses. It may result in higher fees due to less optimal coin selection caused by this added limitation and possibly a larger-than-necessary number of inputs being used. Always enabled for wallets with \"avoid_reuse\" enabled, otherwise default: %u.", DEFAULT_AVOIDPARTIALSPENDS), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-changetype", "What type of change to use (\"legacy\", \"p2sh-segwit\", or \"bech32\"). Default is same as -addresstype, except when -addresstype=p2sh-segwit a native segwit output is used when sending to a native segwit address)", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-disabledandelion", "Disable dandelion support (anonymous transaction relay)", false, OptionsCategory::OPTIONS);
|
||||
argsman.AddArg("-disablewallet", "Do not load the wallet and disable wallet RPC calls", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-discardfee=<amt>", strprintf("The fee rate (in %s/kvB) that indicates your tolerance for discarding change by adding it to the fee (default: %s). "
|
||||
"Note: An output is discarded if it is dust at this rate, but we will always discard up to the dust relay fee and a discard fee above that is limited by the fee estimate for the longest target",
|
||||
CURRENCY_UNIT, FormatMoney(DEFAULT_DISCARD_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
|
||||
argsman.AddArg("-fallbackfee=<amt>", strprintf("A fee rate (in %s/kvB) that will be used when fee estimation has insufficient data. 0 to entirely disable the fallbackfee feature. (default: %s)",
|
||||
CURRENCY_UNIT, FormatMoney(DEFAULT_FALLBACK_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-keypool=<n>", strprintf("Set key pool size to <n> (default: %u). Warning: Smaller sizes may increase the risk of losing funds when restoring from an old backup, if none of the addresses in the original keypool have been used.", DEFAULT_KEYPOOL_SIZE), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-maxapsfee=<n>", strprintf("Spend up to this amount in additional (absolute) fees (in %s) if it allows the use of partial spend avoidance (default: %s)", CURRENCY_UNIT, FormatMoney(DEFAULT_MAX_AVOIDPARTIALSPEND_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-maxtxfee=<amt>", strprintf("Maximum total fees (in %s) to use in a single wallet transaction; setting this too low may abort large transactions (default: %s)",
|
||||
CURRENCY_UNIT, FormatMoney(DEFAULT_TRANSACTION_MAXFEE)), ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
|
||||
argsman.AddArg("-mintxfee=<amt>", strprintf("Fee rates (in %s/kvB) smaller than this are considered zero fee for transaction creation (default: %s)",
|
||||
CURRENCY_UNIT, FormatMoney(DEFAULT_TRANSACTION_MINFEE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-paytxfee=<amt>", strprintf("Fee rate (in %s/kvB) to add to transactions you send (default: %s)",
|
||||
CURRENCY_UNIT, FormatMoney(CFeeRate{DEFAULT_PAY_TX_FEE}.GetFeePerK())), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-rescan", "Rescan the block chain for missing wallet transactions on startup", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
#ifdef ENABLE_EXTERNAL_SIGNER
|
||||
argsman.AddArg("-signer=<cmd>", "External signing tool, see doc/external-signer.md", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
#endif
|
||||
argsman.AddArg("-spendzeroconfchange", strprintf("Spend unconfirmed change when sending transactions (default: %u)", DEFAULT_SPEND_ZEROCONF_CHANGE), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-txconfirmtarget=<n>", strprintf("If paytxfee is not set, include enough fee so transactions begin confirmation on average within n blocks (default: %u)", DEFAULT_TX_CONFIRM_TARGET), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-wallet=<path>", "Specify wallet path to load at startup. Can be used multiple times to load multiple wallets. Path is to a directory containing wallet data and log files. If the path is not absolute, it is interpreted relative to <walletdir>. This only loads existing wallets and does not create new ones. For backwards compatibility this also accepts names of existing top-level data files in <walletdir>.", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-walletbroadcast", strprintf("Make the wallet broadcast transactions (default: %u)", DEFAULT_WALLETBROADCAST), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
argsman.AddArg("-walletdir=<dir>", "Specify directory to hold wallets (default: <datadir>/wallets if it exists, otherwise <datadir>)", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::WALLET);
|
||||
#if HAVE_SYSTEM
|
||||
argsman.AddArg("-walletnotify=<cmd>", "Execute command when a wallet transaction changes. %s in cmd is replaced by TxID, %w is replaced by wallet name, %b is replaced by the hash of the block including the transaction (set to 'unconfirmed' if the transaction is not included) and %h is replaced by the block height (-1 if not included). %w is not currently implemented on windows. On systems where %w is supported, it should NOT be quoted because this would break shell escaping used to invoke the command.", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
#endif
|
||||
argsman.AddArg("-walletrbf", strprintf("Send transactions with full-RBF opt-in enabled (RPC only, default: %u)", DEFAULT_WALLET_RBF), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
|
||||
|
||||
#ifdef USE_BDB
|
||||
argsman.AddArg("-dblogsize=<n>", strprintf("Flush wallet database activity from memory to disk log every <n> megabytes (default: %u)", DEFAULT_WALLET_DBLOGSIZE), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::WALLET_DEBUG_TEST);
|
||||
argsman.AddArg("-flushwallet", strprintf("Run a thread to flush wallet periodically (default: %u)", DEFAULT_FLUSHWALLET), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::WALLET_DEBUG_TEST);
|
||||
argsman.AddArg("-privdb", strprintf("Sets the DB_PRIVATE flag in the wallet db environment (default: %u)", DEFAULT_WALLET_PRIVDB), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::WALLET_DEBUG_TEST);
|
||||
#else
|
||||
argsman.AddHiddenArgs({"-dblogsize", "-flushwallet", "-privdb"});
|
||||
#endif
|
||||
|
||||
#ifdef USE_SQLITE
|
||||
argsman.AddArg("-unsafesqlitesync", "Set SQLite synchronous=OFF to disable waiting for the database to sync to disk. This is unsafe and can cause data loss and corruption. This option is only used by tests to improve their performance (default: false)", ArgsManager::ALLOW_BOOL | ArgsManager::DEBUG_ONLY, OptionsCategory::WALLET_DEBUG_TEST);
|
||||
#else
|
||||
argsman.AddHiddenArgs({"-unsafesqlitesync"});
|
||||
#endif
|
||||
|
||||
argsman.AddArg("-walletrejectlongchains", strprintf("Wallet will not create transactions that violate mempool chain limits (default: %u)", DEFAULT_WALLET_REJECT_LONG_CHAINS), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::WALLET_DEBUG_TEST);
|
||||
|
||||
argsman.AddHiddenArgs({"-zapwallettxes"});
|
||||
}
|
||||
|
||||
bool WalletInit::ParameterInteraction() const
|
||||
{
|
||||
#ifdef USE_BDB
|
||||
if (!BerkeleyDatabaseSanityCheck()) {
|
||||
return InitError(Untranslated("A version conflict was detected between the run-time BerkeleyDB library and the one used during compilation."));
|
||||
}
|
||||
#endif
|
||||
if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) {
|
||||
for (const std::string& wallet : gArgs.GetArgs("-wallet")) {
|
||||
LogPrintf("%s: parameter interaction: -disablewallet -> ignoring -wallet=%s\n", __func__, wallet);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (gArgs.GetBoolArg("-blocksonly", DEFAULT_BLOCKSONLY) && gArgs.SoftSetBoolArg("-walletbroadcast", false)) {
|
||||
LogPrintf("%s: parameter interaction: -blocksonly=1 -> setting -walletbroadcast=0\n", __func__);
|
||||
}
|
||||
|
||||
if (gArgs.IsArgSet("-zapwallettxes")) {
|
||||
return InitError(Untranslated("-zapwallettxes has been removed. If you are attempting to remove a stuck transaction from your wallet, please use abandontransaction instead."));
|
||||
}
|
||||
|
||||
if (gArgs.GetBoolArg("-sysperms", false))
|
||||
return InitError(Untranslated("-sysperms is not allowed in combination with enabled wallet functionality"));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WalletInit::Construct(NodeContext& node) const
|
||||
{
|
||||
ArgsManager& args = *Assert(node.args);
|
||||
if (args.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) {
|
||||
LogPrintf("Wallet disabled!\n");
|
||||
return;
|
||||
}
|
||||
auto wallet_client = interfaces::MakeWalletClient(*node.chain, args);
|
||||
node.wallet_client = wallet_client.get();
|
||||
node.chain_clients.emplace_back(std::move(wallet_client));
|
||||
}
|
||||
43
src/wallet/init.h
Normal file
43
src/wallet/init.h
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2017 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_INIT_H
|
||||
#define DIGIBYTE_WALLET_INIT_H
|
||||
|
||||
#include <string>
|
||||
|
||||
class CRPCTable;
|
||||
class CScheduler;
|
||||
|
||||
//! Return the wallets help message.
|
||||
std::string GetWalletHelpString(bool showDebug);
|
||||
|
||||
//! Wallets parameter interaction
|
||||
bool WalletParameterInteraction();
|
||||
|
||||
//! Register wallet RPCs.
|
||||
void RegisterWalletRPC(CRPCTable &tableRPC);
|
||||
|
||||
//! Responsible for reading and validating the -wallet arguments and verifying the wallet database.
|
||||
// This function will perform salvage on the wallet if requested, as long as only one wallet is
|
||||
// being loaded (WalletParameterInteraction forbids -salvagewallet, -zapwallettxes or -upgradewallet with multiwallet).
|
||||
bool VerifyWallets();
|
||||
|
||||
//! Load wallet databases.
|
||||
bool OpenWallets();
|
||||
|
||||
//! Complete startup of wallets.
|
||||
void StartWallets(CScheduler& scheduler);
|
||||
|
||||
//! Flush all wallets in preparation for shutdown.
|
||||
void FlushWallets();
|
||||
|
||||
//! Stop all wallets. Wallets will be flushed first.
|
||||
void StopWallets();
|
||||
|
||||
//! Close all wallets.
|
||||
void CloseWallets();
|
||||
|
||||
#endif // DIGIBYTE_WALLET_INIT_H
|
||||
586
src/wallet/interfaces.cpp
Normal file
586
src/wallet/interfaces.cpp
Normal file
|
|
@ -0,0 +1,586 @@
|
|||
// Copyright (c) 2018-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <interfaces/wallet.h>
|
||||
|
||||
#include <amount.h>
|
||||
#include <interfaces/chain.h>
|
||||
#include <interfaces/handler.h>
|
||||
#include <policy/fees.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <rpc/server.h>
|
||||
#include <script/standard.h>
|
||||
#include <support/allocators/secure.h>
|
||||
#include <sync.h>
|
||||
#include <uint256.h>
|
||||
#include <util/check.h>
|
||||
#include <util/system.h>
|
||||
#include <util/ui_change_type.h>
|
||||
#include <wallet/context.h>
|
||||
#include <wallet/feebumper.h>
|
||||
#include <wallet/fees.h>
|
||||
#include <wallet/ismine.h>
|
||||
#include <wallet/load.h>
|
||||
#include <wallet/rpcwallet.h>
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using interfaces::Chain;
|
||||
using interfaces::FoundBlock;
|
||||
using interfaces::Handler;
|
||||
using interfaces::MakeHandler;
|
||||
using interfaces::Wallet;
|
||||
using interfaces::WalletAddress;
|
||||
using interfaces::WalletBalances;
|
||||
using interfaces::WalletClient;
|
||||
using interfaces::WalletOrderForm;
|
||||
using interfaces::WalletTx;
|
||||
using interfaces::WalletTxOut;
|
||||
using interfaces::WalletTxStatus;
|
||||
using interfaces::WalletValueMap;
|
||||
|
||||
namespace wallet {
|
||||
namespace {
|
||||
//! Construct wallet tx struct.
|
||||
WalletTx MakeWalletTx(CWallet& wallet, const CWalletTx& wtx)
|
||||
{
|
||||
LOCK(wallet.cs_wallet);
|
||||
WalletTx result;
|
||||
result.tx = wtx.tx;
|
||||
result.txin_is_mine.reserve(wtx.tx->vin.size());
|
||||
for (const auto& txin : wtx.tx->vin) {
|
||||
result.txin_is_mine.emplace_back(wallet.IsMine(txin));
|
||||
}
|
||||
result.txout_is_mine.reserve(wtx.tx->vout.size());
|
||||
result.txout_address.reserve(wtx.tx->vout.size());
|
||||
result.txout_address_is_mine.reserve(wtx.tx->vout.size());
|
||||
for (const auto& txout : wtx.tx->vout) {
|
||||
result.txout_is_mine.emplace_back(wallet.IsMine(txout));
|
||||
result.txout_address.emplace_back();
|
||||
result.txout_address_is_mine.emplace_back(ExtractDestination(txout.scriptPubKey, result.txout_address.back()) ?
|
||||
wallet.IsMine(result.txout_address.back()) :
|
||||
ISMINE_NO);
|
||||
}
|
||||
result.credit = wtx.GetCredit(ISMINE_ALL);
|
||||
result.debit = wtx.GetDebit(ISMINE_ALL);
|
||||
result.change = wtx.GetChange();
|
||||
result.time = wtx.GetTxTime();
|
||||
result.value_map = wtx.mapValue;
|
||||
result.is_coinbase = wtx.IsCoinBase();
|
||||
return result;
|
||||
}
|
||||
|
||||
//! Construct wallet tx status struct.
|
||||
WalletTxStatus MakeWalletTxStatus(const CWallet& wallet, const CWalletTx& wtx)
|
||||
{
|
||||
WalletTxStatus result;
|
||||
result.block_height = wtx.m_confirm.block_height > 0 ? wtx.m_confirm.block_height : std::numeric_limits<int>::max();
|
||||
result.blocks_to_maturity = wtx.GetBlocksToMaturity();
|
||||
result.depth_in_main_chain = wtx.GetDepthInMainChain();
|
||||
result.time_received = wtx.nTimeReceived;
|
||||
result.lock_time = wtx.tx->nLockTime;
|
||||
result.is_final = wallet.chain().checkFinalTx(*wtx.tx);
|
||||
result.is_trusted = wtx.IsTrusted();
|
||||
result.is_abandoned = wtx.isAbandoned();
|
||||
result.is_coinbase = wtx.IsCoinBase();
|
||||
result.is_in_main_chain = wtx.IsInMainChain();
|
||||
return result;
|
||||
}
|
||||
|
||||
//! Construct wallet TxOut struct.
|
||||
WalletTxOut MakeWalletTxOut(const CWallet& wallet,
|
||||
const CWalletTx& wtx,
|
||||
int n,
|
||||
int depth) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
|
||||
{
|
||||
WalletTxOut result;
|
||||
result.txout = wtx.tx->vout[n];
|
||||
result.time = wtx.GetTxTime();
|
||||
result.depth_in_main_chain = depth;
|
||||
result.is_spent = wallet.IsSpent(wtx.GetHash(), n);
|
||||
return result;
|
||||
}
|
||||
|
||||
class WalletImpl : public Wallet
|
||||
{
|
||||
public:
|
||||
explicit WalletImpl(const std::shared_ptr<CWallet>& wallet) : m_wallet(wallet) {}
|
||||
|
||||
bool encryptWallet(const SecureString& wallet_passphrase) override
|
||||
{
|
||||
return m_wallet->EncryptWallet(wallet_passphrase);
|
||||
}
|
||||
bool isCrypted() override { return m_wallet->IsCrypted(); }
|
||||
bool lock() override { return m_wallet->Lock(); }
|
||||
bool unlock(const SecureString& wallet_passphrase) override { return m_wallet->Unlock(wallet_passphrase); }
|
||||
bool isLocked() override { return m_wallet->IsLocked(); }
|
||||
bool changeWalletPassphrase(const SecureString& old_wallet_passphrase,
|
||||
const SecureString& new_wallet_passphrase) override
|
||||
{
|
||||
return m_wallet->ChangeWalletPassphrase(old_wallet_passphrase, new_wallet_passphrase);
|
||||
}
|
||||
void abortRescan() override { m_wallet->AbortRescan(); }
|
||||
bool backupWallet(const std::string& filename) override { return m_wallet->BackupWallet(filename); }
|
||||
std::string getWalletName() override { return m_wallet->GetName(); }
|
||||
bool getNewDestination(const OutputType type, const std::string label, CTxDestination& dest) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
std::string error;
|
||||
return m_wallet->GetNewDestination(type, label, dest, error);
|
||||
}
|
||||
bool getPubKey(const CScript& script, const CKeyID& address, CPubKey& pub_key) override
|
||||
{
|
||||
std::unique_ptr<SigningProvider> provider = m_wallet->GetSolvingProvider(script);
|
||||
if (provider) {
|
||||
return provider->GetPubKey(address, pub_key);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
SigningResult signMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) override
|
||||
{
|
||||
return m_wallet->SignMessage(message, pkhash, str_sig);
|
||||
}
|
||||
bool isSpendable(const CTxDestination& dest) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->IsMine(dest) & ISMINE_SPENDABLE;
|
||||
}
|
||||
bool haveWatchOnly() override
|
||||
{
|
||||
auto spk_man = m_wallet->GetLegacyScriptPubKeyMan();
|
||||
if (spk_man) {
|
||||
return spk_man->HaveWatchOnly();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
bool setAddressBook(const CTxDestination& dest, const std::string& name, const std::string& purpose) override
|
||||
{
|
||||
return m_wallet->SetAddressBook(dest, name, purpose);
|
||||
}
|
||||
bool delAddressBook(const CTxDestination& dest) override
|
||||
{
|
||||
return m_wallet->DelAddressBook(dest);
|
||||
}
|
||||
bool getAddress(const CTxDestination& dest,
|
||||
std::string* name,
|
||||
isminetype* is_mine,
|
||||
std::string* purpose) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
auto it = m_wallet->m_address_book.find(dest);
|
||||
if (it == m_wallet->m_address_book.end() || it->second.IsChange()) {
|
||||
return false;
|
||||
}
|
||||
if (name) {
|
||||
*name = it->second.GetLabel();
|
||||
}
|
||||
if (is_mine) {
|
||||
*is_mine = m_wallet->IsMine(dest);
|
||||
}
|
||||
if (purpose) {
|
||||
*purpose = it->second.purpose;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::vector<WalletAddress> getAddresses() override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
std::vector<WalletAddress> result;
|
||||
for (const auto& item : m_wallet->m_address_book) {
|
||||
if (item.second.IsChange()) continue;
|
||||
result.emplace_back(item.first, m_wallet->IsMine(item.first), item.second.GetLabel(), item.second.purpose);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
std::vector<std::string> getAddressReceiveRequests() override {
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->GetAddressReceiveRequests();
|
||||
}
|
||||
bool setAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& value) override {
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
WalletBatch batch{m_wallet->GetDatabase()};
|
||||
return m_wallet->SetAddressReceiveRequest(batch, dest, id, value);
|
||||
}
|
||||
bool displayAddress(const CTxDestination& dest) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->DisplayAddress(dest);
|
||||
}
|
||||
void lockCoin(const COutPoint& output) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->LockCoin(output);
|
||||
}
|
||||
void unlockCoin(const COutPoint& output) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->UnlockCoin(output);
|
||||
}
|
||||
bool isLockedCoin(const COutPoint& output) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->IsLockedCoin(output.hash, output.n);
|
||||
}
|
||||
void listLockedCoins(std::vector<COutPoint>& outputs) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->ListLockedCoins(outputs);
|
||||
}
|
||||
CTransactionRef createTransaction(const std::vector<CRecipient>& recipients,
|
||||
const CCoinControl& coin_control,
|
||||
bool sign,
|
||||
int& change_pos,
|
||||
CAmount& fee,
|
||||
bilingual_str& fail_reason) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
CTransactionRef tx;
|
||||
FeeCalculation fee_calc_out;
|
||||
if (!m_wallet->CreateTransaction(recipients, tx, fee, change_pos,
|
||||
fail_reason, coin_control, fee_calc_out, sign)) {
|
||||
return {};
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
void commitTransaction(CTransactionRef tx,
|
||||
WalletValueMap value_map,
|
||||
WalletOrderForm order_form) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
m_wallet->CommitTransaction(std::move(tx), std::move(value_map), std::move(order_form));
|
||||
}
|
||||
bool transactionCanBeAbandoned(const uint256& txid) override { return m_wallet->TransactionCanBeAbandoned(txid); }
|
||||
bool abandonTransaction(const uint256& txid) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->AbandonTransaction(txid);
|
||||
}
|
||||
bool transactionCanBeBumped(const uint256& txid) override
|
||||
{
|
||||
return feebumper::TransactionCanBeBumped(*m_wallet.get(), txid);
|
||||
}
|
||||
bool createBumpTransaction(const uint256& txid,
|
||||
const CCoinControl& coin_control,
|
||||
std::vector<bilingual_str>& errors,
|
||||
CAmount& old_fee,
|
||||
CAmount& new_fee,
|
||||
CMutableTransaction& mtx) override
|
||||
{
|
||||
return feebumper::CreateRateBumpTransaction(*m_wallet.get(), txid, coin_control, errors, old_fee, new_fee, mtx) == feebumper::Result::OK;
|
||||
}
|
||||
bool signBumpTransaction(CMutableTransaction& mtx) override { return feebumper::SignTransaction(*m_wallet.get(), mtx); }
|
||||
bool commitBumpTransaction(const uint256& txid,
|
||||
CMutableTransaction&& mtx,
|
||||
std::vector<bilingual_str>& errors,
|
||||
uint256& bumped_txid) override
|
||||
{
|
||||
return feebumper::CommitTransaction(*m_wallet.get(), txid, std::move(mtx), errors, bumped_txid) ==
|
||||
feebumper::Result::OK;
|
||||
}
|
||||
CTransactionRef getTx(const uint256& txid) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
auto mi = m_wallet->mapWallet.find(txid);
|
||||
if (mi != m_wallet->mapWallet.end()) {
|
||||
return mi->second.tx;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
WalletTx getWalletTx(const uint256& txid) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
auto mi = m_wallet->mapWallet.find(txid);
|
||||
if (mi != m_wallet->mapWallet.end()) {
|
||||
return MakeWalletTx(*m_wallet, mi->second);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<WalletTx> getWalletTxs() override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
std::vector<WalletTx> result;
|
||||
result.reserve(m_wallet->mapWallet.size());
|
||||
for (const auto& entry : m_wallet->mapWallet) {
|
||||
result.emplace_back(MakeWalletTx(*m_wallet, entry.second));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
bool tryGetTxStatus(const uint256& txid,
|
||||
interfaces::WalletTxStatus& tx_status,
|
||||
int& num_blocks,
|
||||
int64_t& block_time) override
|
||||
{
|
||||
TRY_LOCK(m_wallet->cs_wallet, locked_wallet);
|
||||
if (!locked_wallet) {
|
||||
return false;
|
||||
}
|
||||
auto mi = m_wallet->mapWallet.find(txid);
|
||||
if (mi == m_wallet->mapWallet.end()) {
|
||||
return false;
|
||||
}
|
||||
num_blocks = m_wallet->GetLastBlockHeight();
|
||||
block_time = -1;
|
||||
CHECK_NONFATAL(m_wallet->chain().findBlock(m_wallet->GetLastBlockHash(), FoundBlock().time(block_time)));
|
||||
tx_status = MakeWalletTxStatus(*m_wallet, mi->second);
|
||||
return true;
|
||||
}
|
||||
WalletTx getWalletTxDetails(const uint256& txid,
|
||||
WalletTxStatus& tx_status,
|
||||
WalletOrderForm& order_form,
|
||||
bool& in_mempool,
|
||||
int& num_blocks) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
auto mi = m_wallet->mapWallet.find(txid);
|
||||
if (mi != m_wallet->mapWallet.end()) {
|
||||
num_blocks = m_wallet->GetLastBlockHeight();
|
||||
in_mempool = mi->second.InMempool();
|
||||
order_form = mi->second.vOrderForm;
|
||||
tx_status = MakeWalletTxStatus(*m_wallet, mi->second);
|
||||
return MakeWalletTx(*m_wallet, mi->second);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
TransactionError fillPSBT(int sighash_type,
|
||||
bool sign,
|
||||
bool bip32derivs,
|
||||
size_t* n_signed,
|
||||
PartiallySignedTransaction& psbtx,
|
||||
bool& complete) override
|
||||
{
|
||||
return m_wallet->FillPSBT(psbtx, complete, sighash_type, sign, bip32derivs, n_signed);
|
||||
}
|
||||
WalletBalances getBalances() override
|
||||
{
|
||||
const auto bal = m_wallet->GetBalance();
|
||||
WalletBalances result;
|
||||
result.balance = bal.m_mine_trusted;
|
||||
result.unconfirmed_balance = bal.m_mine_untrusted_pending;
|
||||
result.immature_balance = bal.m_mine_immature;
|
||||
result.have_watch_only = haveWatchOnly();
|
||||
if (result.have_watch_only) {
|
||||
result.watch_only_balance = bal.m_watchonly_trusted;
|
||||
result.unconfirmed_watch_only_balance = bal.m_watchonly_untrusted_pending;
|
||||
result.immature_watch_only_balance = bal.m_watchonly_immature;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
bool tryGetBalances(WalletBalances& balances, uint256& block_hash) override
|
||||
{
|
||||
TRY_LOCK(m_wallet->cs_wallet, locked_wallet);
|
||||
if (!locked_wallet) {
|
||||
return false;
|
||||
}
|
||||
block_hash = m_wallet->GetLastBlockHash();
|
||||
balances = getBalances();
|
||||
return true;
|
||||
}
|
||||
CAmount getBalance() override { return m_wallet->GetBalance().m_mine_trusted; }
|
||||
CAmount getAvailableBalance(const CCoinControl& coin_control) override
|
||||
{
|
||||
return m_wallet->GetAvailableBalance(&coin_control);
|
||||
}
|
||||
isminetype txinIsMine(const CTxIn& txin) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->IsMine(txin);
|
||||
}
|
||||
isminetype txoutIsMine(const CTxOut& txout) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->IsMine(txout);
|
||||
}
|
||||
CAmount getDebit(const CTxIn& txin, isminefilter filter) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->GetDebit(txin, filter);
|
||||
}
|
||||
CAmount getCredit(const CTxOut& txout, isminefilter filter) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
return m_wallet->GetCredit(txout, filter);
|
||||
}
|
||||
CoinsList listCoins() override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
CoinsList result;
|
||||
for (const auto& entry : m_wallet->ListCoins()) {
|
||||
auto& group = result[entry.first];
|
||||
for (const auto& coin : entry.second) {
|
||||
group.emplace_back(COutPoint(coin.tx->GetHash(), coin.i),
|
||||
MakeWalletTxOut(*m_wallet, *coin.tx, coin.i, coin.nDepth));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
std::vector<WalletTxOut> getCoins(const std::vector<COutPoint>& outputs) override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
std::vector<WalletTxOut> result;
|
||||
result.reserve(outputs.size());
|
||||
for (const auto& output : outputs) {
|
||||
result.emplace_back();
|
||||
auto it = m_wallet->mapWallet.find(output.hash);
|
||||
if (it != m_wallet->mapWallet.end()) {
|
||||
int depth = it->second.GetDepthInMainChain();
|
||||
if (depth >= 0) {
|
||||
result.back() = MakeWalletTxOut(*m_wallet, it->second, output.n, depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
CAmount getRequiredFee(unsigned int tx_bytes) override { return GetRequiredFee(*m_wallet, tx_bytes); }
|
||||
CAmount getMinimumFee(unsigned int tx_bytes,
|
||||
const CCoinControl& coin_control,
|
||||
int* returned_target,
|
||||
FeeReason* reason) override
|
||||
{
|
||||
FeeCalculation fee_calc;
|
||||
CAmount result;
|
||||
result = GetMinimumFee(*m_wallet, tx_bytes, coin_control, &fee_calc);
|
||||
if (returned_target) *returned_target = fee_calc.returnedTarget;
|
||||
if (reason) *reason = fee_calc.reason;
|
||||
return result;
|
||||
}
|
||||
unsigned int getConfirmTarget() override { return m_wallet->m_confirm_target; }
|
||||
bool hdEnabled() override { return m_wallet->IsHDEnabled(); }
|
||||
bool canGetAddresses() override { return m_wallet->CanGetAddresses(); }
|
||||
bool hasExternalSigner() override { return m_wallet->IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER); }
|
||||
bool privateKeysDisabled() override { return m_wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); }
|
||||
OutputType getDefaultAddressType() override { return m_wallet->m_default_address_type; }
|
||||
CAmount getDefaultMaxTxFee() override { return m_wallet->m_default_max_tx_fee; }
|
||||
void remove() override
|
||||
{
|
||||
RemoveWallet(m_wallet, false /* load_on_start */);
|
||||
}
|
||||
bool isLegacy() override { return m_wallet->IsLegacy(); }
|
||||
std::unique_ptr<Handler> handleUnload(UnloadFn fn) override
|
||||
{
|
||||
return MakeHandler(m_wallet->NotifyUnload.connect(fn));
|
||||
}
|
||||
std::unique_ptr<Handler> handleShowProgress(ShowProgressFn fn) override
|
||||
{
|
||||
return MakeHandler(m_wallet->ShowProgress.connect(fn));
|
||||
}
|
||||
std::unique_ptr<Handler> handleStatusChanged(StatusChangedFn fn) override
|
||||
{
|
||||
return MakeHandler(m_wallet->NotifyStatusChanged.connect([fn](CWallet*) { fn(); }));
|
||||
}
|
||||
std::unique_ptr<Handler> handleAddressBookChanged(AddressBookChangedFn fn) override
|
||||
{
|
||||
return MakeHandler(m_wallet->NotifyAddressBookChanged.connect(
|
||||
[fn](const CTxDestination& address, const std::string& label, bool is_mine,
|
||||
const std::string& purpose, ChangeType status) { fn(address, label, is_mine, purpose, status); }));
|
||||
}
|
||||
std::unique_ptr<Handler> handleTransactionChanged(TransactionChangedFn fn) override
|
||||
{
|
||||
return MakeHandler(m_wallet->NotifyTransactionChanged.connect(
|
||||
[fn](const uint256& txid, ChangeType status) { fn(txid, status); }));
|
||||
}
|
||||
std::unique_ptr<Handler> handleWatchOnlyChanged(WatchOnlyChangedFn fn) override
|
||||
{
|
||||
return MakeHandler(m_wallet->NotifyWatchonlyChanged.connect(fn));
|
||||
}
|
||||
std::unique_ptr<Handler> handleCanGetAddressesChanged(CanGetAddressesChangedFn fn) override
|
||||
{
|
||||
return MakeHandler(m_wallet->NotifyCanGetAddressesChanged.connect(fn));
|
||||
}
|
||||
CWallet* wallet() override { return m_wallet.get(); }
|
||||
|
||||
std::shared_ptr<CWallet> m_wallet;
|
||||
};
|
||||
|
||||
class WalletClientImpl : public WalletClient
|
||||
{
|
||||
public:
|
||||
WalletClientImpl(Chain& chain, ArgsManager& args)
|
||||
{
|
||||
m_context.chain = &chain;
|
||||
m_context.args = &args;
|
||||
}
|
||||
~WalletClientImpl() override { UnloadWallets(); }
|
||||
|
||||
//! ChainClient methods
|
||||
void registerRpcs() override
|
||||
{
|
||||
for (const CRPCCommand& command : GetWalletRPCCommands()) {
|
||||
m_rpc_commands.emplace_back(command.category, command.name, [this, &command](const JSONRPCRequest& request, UniValue& result, bool last_handler) {
|
||||
JSONRPCRequest wallet_request = request;
|
||||
wallet_request.context = &m_context;
|
||||
return command.actor(wallet_request, result, last_handler);
|
||||
}, command.argNames, command.unique_id);
|
||||
m_rpc_handlers.emplace_back(m_context.chain->handleRpc(m_rpc_commands.back()));
|
||||
}
|
||||
}
|
||||
bool verify() override { return VerifyWallets(*m_context.chain); }
|
||||
bool load() override { return LoadWallets(*m_context.chain); }
|
||||
void start(CScheduler& scheduler) override { return StartWallets(scheduler, *Assert(m_context.args)); }
|
||||
void flush() override { return FlushWallets(); }
|
||||
void stop() override { return StopWallets(); }
|
||||
void setMockTime(int64_t time) override { return SetMockTime(time); }
|
||||
|
||||
//! WalletClient methods
|
||||
std::unique_ptr<Wallet> createWallet(const std::string& name, const SecureString& passphrase, uint64_t wallet_creation_flags, bilingual_str& error, std::vector<bilingual_str>& warnings) override
|
||||
{
|
||||
std::shared_ptr<CWallet> wallet;
|
||||
DatabaseOptions options;
|
||||
DatabaseStatus status;
|
||||
options.require_create = true;
|
||||
options.create_flags = wallet_creation_flags;
|
||||
options.create_passphrase = passphrase;
|
||||
return MakeWallet(CreateWallet(*m_context.chain, name, true /* load_on_start */, options, status, error, warnings));
|
||||
}
|
||||
std::unique_ptr<Wallet> loadWallet(const std::string& name, bilingual_str& error, std::vector<bilingual_str>& warnings) override
|
||||
{
|
||||
DatabaseOptions options;
|
||||
DatabaseStatus status;
|
||||
options.require_existing = true;
|
||||
return MakeWallet(LoadWallet(*m_context.chain, name, true /* load_on_start */, options, status, error, warnings));
|
||||
}
|
||||
std::string getWalletDir() override
|
||||
{
|
||||
return GetWalletDir().string();
|
||||
}
|
||||
std::vector<std::string> listWalletDir() override
|
||||
{
|
||||
std::vector<std::string> paths;
|
||||
for (auto& path : ListDatabases(GetWalletDir())) {
|
||||
paths.push_back(path.string());
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
std::vector<std::unique_ptr<Wallet>> getWallets() override
|
||||
{
|
||||
std::vector<std::unique_ptr<Wallet>> wallets;
|
||||
for (const auto& wallet : GetWallets()) {
|
||||
wallets.emplace_back(MakeWallet(wallet));
|
||||
}
|
||||
return wallets;
|
||||
}
|
||||
std::unique_ptr<Handler> handleLoadWallet(LoadWalletFn fn) override
|
||||
{
|
||||
return HandleLoadWallet(std::move(fn));
|
||||
}
|
||||
|
||||
WalletContext m_context;
|
||||
const std::vector<std::string> m_wallet_filenames;
|
||||
std::vector<std::unique_ptr<Handler>> m_rpc_handlers;
|
||||
std::list<CRPCCommand> m_rpc_commands;
|
||||
};
|
||||
} // namespace
|
||||
} // namespace wallet
|
||||
|
||||
namespace interfaces {
|
||||
std::unique_ptr<Wallet> MakeWallet(const std::shared_ptr<CWallet>& wallet) { return wallet ? std::make_unique<wallet::WalletImpl>(wallet) : nullptr; }
|
||||
|
||||
std::unique_ptr<WalletClient> MakeWalletClient(Chain& chain, ArgsManager& args)
|
||||
{
|
||||
return std::make_unique<wallet::WalletClientImpl>(chain, args);
|
||||
}
|
||||
} // namespace interfaces
|
||||
70
src/wallet/ismine.h
Normal file
70
src/wallet/ismine.h
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2019 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_ISMINE_H
|
||||
#define DIGIBYTE_WALLET_ISMINE_H
|
||||
|
||||
#include <script/standard.h>
|
||||
|
||||
#include <stdint.h>
|
||||
#include <bitset>
|
||||
|
||||
class CWallet;
|
||||
class CScript;
|
||||
|
||||
/**
|
||||
* IsMine() return codes, which depend on ScriptPubKeyMan implementation.
|
||||
* Not every ScriptPubKeyMan covers all types, please refer to
|
||||
* https://github.com/digibyte/digibyte/blob/master/doc/release-notes/release-notes-0.21.0.md#ismine-semantics
|
||||
* for better understanding.
|
||||
*
|
||||
* For LegacyScriptPubKeyMan,
|
||||
* ISMINE_NO: the scriptPubKey is not in the wallet;
|
||||
* ISMINE_WATCH_ONLY: the scriptPubKey has been imported into the wallet;
|
||||
* ISMINE_SPENDABLE: the scriptPubKey corresponds to an address owned by the wallet user (can spend with the private key);
|
||||
* ISMINE_USED: the scriptPubKey corresponds to a used address owned by the wallet user;
|
||||
* ISMINE_ALL: all ISMINE flags except for USED;
|
||||
* ISMINE_ALL_USED: all ISMINE flags including USED;
|
||||
* ISMINE_ENUM_ELEMENTS: the number of isminetype enum elements.
|
||||
*
|
||||
* For DescriptorScriptPubKeyMan and future ScriptPubKeyMan,
|
||||
* ISMINE_NO: the scriptPubKey is not in the wallet;
|
||||
* ISMINE_SPENDABLE: the scriptPubKey matches a scriptPubKey in the wallet.
|
||||
* ISMINE_USED: the scriptPubKey corresponds to a used address owned by the wallet user.
|
||||
*
|
||||
*/
|
||||
enum isminetype : unsigned int
|
||||
{
|
||||
ISMINE_NO = 0,
|
||||
ISMINE_WATCH_ONLY = 1 << 0,
|
||||
ISMINE_SPENDABLE = 1 << 1,
|
||||
ISMINE_USED = 1 << 2,
|
||||
ISMINE_ALL = ISMINE_WATCH_ONLY | ISMINE_SPENDABLE,
|
||||
ISMINE_ALL_USED = ISMINE_ALL | ISMINE_USED,
|
||||
ISMINE_ENUM_ELEMENTS,
|
||||
};
|
||||
/** used for bitflags of isminetype */
|
||||
typedef uint8_t isminefilter;
|
||||
|
||||
/**
|
||||
* Cachable amount subdivided into watchonly and spendable parts.
|
||||
*/
|
||||
struct CachableAmount
|
||||
{
|
||||
// NO and ALL are never (supposed to be) cached
|
||||
std::bitset<ISMINE_ENUM_ELEMENTS> m_cached;
|
||||
CAmount m_value[ISMINE_ENUM_ELEMENTS];
|
||||
inline void Reset()
|
||||
{
|
||||
m_cached.reset();
|
||||
}
|
||||
void Set(isminefilter filter, CAmount value)
|
||||
{
|
||||
m_cached.set(filter);
|
||||
m_value[filter] = value;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // DIGIBYTE_WALLET_ISMINE_H
|
||||
161
src/wallet/load.cpp
Normal file
161
src/wallet/load.cpp
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <wallet/load.h>
|
||||
|
||||
#include <fs.h>
|
||||
#include <interfaces/chain.h>
|
||||
#include <scheduler.h>
|
||||
#include <util/string.h>
|
||||
#include <util/system.h>
|
||||
#include <util/translation.h>
|
||||
#include <wallet/wallet.h>
|
||||
#include <wallet/walletdb.h>
|
||||
|
||||
#include <univalue.h>
|
||||
|
||||
bool VerifyWallets(interfaces::Chain& chain)
|
||||
{
|
||||
if (gArgs.IsArgSet("-walletdir")) {
|
||||
fs::path wallet_dir = gArgs.GetArg("-walletdir", "");
|
||||
boost::system::error_code error;
|
||||
// The canonical path cleans the path, preventing >1 Berkeley environment instances for the same directory
|
||||
fs::path canonical_wallet_dir = fs::canonical(wallet_dir, error).remove_trailing_separator();
|
||||
if (error || !fs::exists(wallet_dir)) {
|
||||
chain.initError(strprintf(_("Specified -walletdir \"%s\" does not exist"), wallet_dir.string()));
|
||||
return false;
|
||||
} else if (!fs::is_directory(wallet_dir)) {
|
||||
chain.initError(strprintf(_("Specified -walletdir \"%s\" is not a directory"), wallet_dir.string()));
|
||||
return false;
|
||||
// The canonical path transforms relative paths into absolute ones, so we check the non-canonical version
|
||||
} else if (!wallet_dir.is_absolute()) {
|
||||
chain.initError(strprintf(_("Specified -walletdir \"%s\" is a relative path"), wallet_dir.string()));
|
||||
return false;
|
||||
}
|
||||
gArgs.ForceSetArg("-walletdir", canonical_wallet_dir.string());
|
||||
}
|
||||
|
||||
LogPrintf("Using wallet directory %s\n", GetWalletDir().string());
|
||||
|
||||
chain.initMessage(_("Verifying wallet(s)…").translated);
|
||||
|
||||
// For backwards compatibility if an unnamed top level wallet exists in the
|
||||
// wallets directory, include it in the default list of wallets to load.
|
||||
if (!gArgs.IsArgSet("wallet")) {
|
||||
DatabaseOptions options;
|
||||
DatabaseStatus status;
|
||||
bilingual_str error_string;
|
||||
options.require_existing = true;
|
||||
options.verify = false;
|
||||
if (MakeWalletDatabase("", options, status, error_string)) {
|
||||
gArgs.LockSettings([&](util::Settings& settings) {
|
||||
util::SettingsValue wallets(util::SettingsValue::VARR);
|
||||
wallets.push_back(""); // Default wallet name is ""
|
||||
settings.rw_settings["wallet"] = wallets;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Keep track of each wallet absolute path to detect duplicates.
|
||||
std::set<fs::path> wallet_paths;
|
||||
|
||||
for (const auto& wallet_file : gArgs.GetArgs("-wallet")) {
|
||||
const fs::path path = fsbridge::AbsPathJoin(GetWalletDir(), wallet_file);
|
||||
|
||||
if (!wallet_paths.insert(path).second) {
|
||||
chain.initWarning(strprintf(_("Ignoring duplicate -wallet %s."), wallet_file));
|
||||
continue;
|
||||
}
|
||||
|
||||
DatabaseOptions options;
|
||||
DatabaseStatus status;
|
||||
options.require_existing = true;
|
||||
options.verify = true;
|
||||
bilingual_str error_string;
|
||||
if (!MakeWalletDatabase(wallet_file, options, status, error_string)) {
|
||||
if (status == DatabaseStatus::FAILED_NOT_FOUND) {
|
||||
chain.initWarning(Untranslated(strprintf("Skipping -wallet path that doesn't exist. %s", error_string.original)));
|
||||
} else {
|
||||
chain.initError(error_string);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LoadWallets(interfaces::Chain& chain)
|
||||
{
|
||||
try {
|
||||
std::set<fs::path> wallet_paths;
|
||||
for (const std::string& name : gArgs.GetArgs("-wallet")) {
|
||||
if (!wallet_paths.insert(name).second) {
|
||||
continue;
|
||||
}
|
||||
DatabaseOptions options;
|
||||
DatabaseStatus status;
|
||||
options.require_existing = true;
|
||||
options.verify = false; // No need to verify, assuming verified earlier in VerifyWallets()
|
||||
bilingual_str error;
|
||||
std::vector<bilingual_str> warnings;
|
||||
std::unique_ptr<WalletDatabase> database = MakeWalletDatabase(name, options, status, error);
|
||||
if (!database && status == DatabaseStatus::FAILED_NOT_FOUND) {
|
||||
continue;
|
||||
}
|
||||
chain.initMessage(_("Loading wallet…").translated);
|
||||
std::shared_ptr<CWallet> pwallet = database ? CWallet::Create(&chain, name, std::move(database), options.create_flags, error, warnings) : nullptr;
|
||||
if (!warnings.empty()) chain.initWarning(Join(warnings, Untranslated("\n")));
|
||||
if (!pwallet) {
|
||||
chain.initError(error);
|
||||
return false;
|
||||
}
|
||||
AddWallet(pwallet);
|
||||
}
|
||||
return true;
|
||||
} catch (const std::runtime_error& e) {
|
||||
chain.initError(Untranslated(e.what()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void StartWallets(CScheduler& scheduler, const ArgsManager& args)
|
||||
{
|
||||
for (const std::shared_ptr<CWallet>& pwallet : GetWallets()) {
|
||||
pwallet->postInitProcess();
|
||||
}
|
||||
|
||||
// Schedule periodic wallet flushes and tx rebroadcasts
|
||||
if (args.GetBoolArg("-flushwallet", DEFAULT_FLUSHWALLET)) {
|
||||
scheduler.scheduleEvery(MaybeCompactWalletDB, std::chrono::milliseconds{500});
|
||||
}
|
||||
scheduler.scheduleEvery(MaybeResendWalletTxs, std::chrono::milliseconds{1000});
|
||||
}
|
||||
|
||||
void FlushWallets()
|
||||
{
|
||||
for (const std::shared_ptr<CWallet>& pwallet : GetWallets()) {
|
||||
pwallet->Flush();
|
||||
}
|
||||
}
|
||||
|
||||
void StopWallets()
|
||||
{
|
||||
for (const std::shared_ptr<CWallet>& pwallet : GetWallets()) {
|
||||
pwallet->Close();
|
||||
}
|
||||
}
|
||||
|
||||
void UnloadWallets()
|
||||
{
|
||||
auto wallets = GetWallets();
|
||||
while (!wallets.empty()) {
|
||||
auto wallet = wallets.back();
|
||||
wallets.pop_back();
|
||||
std::vector<bilingual_str> warnings;
|
||||
RemoveWallet(wallet, std::nullopt, warnings);
|
||||
UnloadWallet(std::move(wallet));
|
||||
}
|
||||
}
|
||||
37
src/wallet/load.h
Normal file
37
src/wallet/load.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_LOAD_H
|
||||
#define DIGIBYTE_WALLET_LOAD_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class ArgsManager;
|
||||
class CScheduler;
|
||||
|
||||
namespace interfaces {
|
||||
class Chain;
|
||||
} // namespace interfaces
|
||||
|
||||
//! Responsible for reading and validating the -wallet arguments and verifying the wallet database.
|
||||
bool VerifyWallets(interfaces::Chain& chain);
|
||||
|
||||
//! Load wallet databases.
|
||||
bool LoadWallets(interfaces::Chain& chain);
|
||||
|
||||
//! Complete startup of wallets.
|
||||
void StartWallets(CScheduler& scheduler, const ArgsManager& args);
|
||||
|
||||
//! Flush all wallets in preparation for shutdown.
|
||||
void FlushWallets();
|
||||
|
||||
//! Stop all wallets. Wallets will be flushed first.
|
||||
void StopWallets();
|
||||
|
||||
//! Close all wallets.
|
||||
void UnloadWallets();
|
||||
|
||||
#endif // DIGIBYTE_WALLET_LOAD_H
|
||||
471
src/wallet/receive.cpp
Normal file
471
src/wallet/receive.cpp
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
// Copyright (c) 2021 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <consensus/consensus.h>
|
||||
#include <wallet/receive.h>
|
||||
#include <wallet/transaction.h>
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
isminetype CWallet::IsMine(const CTxIn &txin) const
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
std::map<uint256, CWalletTx>::const_iterator mi = mapWallet.find(txin.prevout.hash);
|
||||
if (mi != mapWallet.end())
|
||||
{
|
||||
const CWalletTx& prev = (*mi).second;
|
||||
if (txin.prevout.n < prev.tx->vout.size())
|
||||
return IsMine(prev.tx->vout[txin.prevout.n]);
|
||||
}
|
||||
return ISMINE_NO;
|
||||
}
|
||||
|
||||
bool CWallet::IsAllFromMe(const CTransaction& tx, const isminefilter& filter) const
|
||||
{
|
||||
LOCK(cs_wallet);
|
||||
|
||||
for (const CTxIn& txin : tx.vin)
|
||||
{
|
||||
auto mi = mapWallet.find(txin.prevout.hash);
|
||||
if (mi == mapWallet.end())
|
||||
return false; // any unknown inputs can't be from us
|
||||
|
||||
const CWalletTx& prev = (*mi).second;
|
||||
|
||||
if (txin.prevout.n >= prev.tx->vout.size())
|
||||
return false; // invalid input!
|
||||
|
||||
if (!(IsMine(prev.tx->vout[txin.prevout.n]) & filter))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
CAmount CWallet::GetCredit(const CTxOut& txout, const isminefilter& filter) const
|
||||
{
|
||||
if (!MoneyRange(txout.nValue))
|
||||
throw std::runtime_error(std::string(__func__) + ": value out of range");
|
||||
LOCK(cs_wallet);
|
||||
return ((IsMine(txout) & filter) ? txout.nValue : 0);
|
||||
}
|
||||
|
||||
CAmount CWallet::GetCredit(const CTransaction& tx, const isminefilter& filter) const
|
||||
{
|
||||
CAmount nCredit = 0;
|
||||
for (const CTxOut& txout : tx.vout)
|
||||
{
|
||||
nCredit += GetCredit(txout, filter);
|
||||
if (!MoneyRange(nCredit))
|
||||
throw std::runtime_error(std::string(__func__) + ": value out of range");
|
||||
}
|
||||
return nCredit;
|
||||
}
|
||||
|
||||
bool CWallet::IsChange(const CScript& script) const
|
||||
{
|
||||
// TODO: fix handling of 'change' outputs. The assumption is that any
|
||||
// payment to a script that is ours, but is not in the address book
|
||||
// is change. That assumption is likely to break when we implement multisignature
|
||||
// wallets that return change back into a multi-signature-protected address;
|
||||
// a better way of identifying which outputs are 'the send' and which are
|
||||
// 'the change' will need to be implemented (maybe extend CWalletTx to remember
|
||||
// which output, if any, was change).
|
||||
AssertLockHeld(cs_wallet);
|
||||
if (IsMine(script))
|
||||
{
|
||||
CTxDestination address;
|
||||
if (!ExtractDestination(script, address))
|
||||
return true;
|
||||
if (!FindAddressBookEntry(address)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CWallet::IsChange(const CTxOut& txout) const
|
||||
{
|
||||
return IsChange(txout.scriptPubKey);
|
||||
}
|
||||
|
||||
CAmount CWallet::GetChange(const CTxOut& txout) const
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
if (!MoneyRange(txout.nValue))
|
||||
throw std::runtime_error(std::string(__func__) + ": value out of range");
|
||||
return (IsChange(txout) ? txout.nValue : 0);
|
||||
}
|
||||
|
||||
CAmount CWallet::GetChange(const CTransaction& tx) const
|
||||
{
|
||||
LOCK(cs_wallet);
|
||||
CAmount nChange = 0;
|
||||
for (const CTxOut& txout : tx.vout)
|
||||
{
|
||||
nChange += GetChange(txout);
|
||||
if (!MoneyRange(nChange))
|
||||
throw std::runtime_error(std::string(__func__) + ": value out of range");
|
||||
}
|
||||
return nChange;
|
||||
}
|
||||
|
||||
CAmount CWalletTx::GetCachableAmount(AmountType type, const isminefilter& filter, bool recalculate) const
|
||||
{
|
||||
auto& amount = m_amounts[type];
|
||||
if (recalculate || !amount.m_cached[filter]) {
|
||||
amount.Set(filter, type == DEBIT ? pwallet->GetDebit(*tx, filter) : pwallet->GetCredit(*tx, filter));
|
||||
m_is_cache_empty = false;
|
||||
}
|
||||
return amount.m_value[filter];
|
||||
}
|
||||
|
||||
CAmount CWalletTx::GetCredit(const isminefilter& filter) const
|
||||
{
|
||||
// Must wait until coinbase is safely deep enough in the chain before valuing it
|
||||
if (IsImmatureCoinBase())
|
||||
return 0;
|
||||
|
||||
CAmount credit = 0;
|
||||
if (filter & ISMINE_SPENDABLE) {
|
||||
// GetBalance can assume transactions in mapWallet won't change
|
||||
credit += GetCachableAmount(CREDIT, ISMINE_SPENDABLE);
|
||||
}
|
||||
if (filter & ISMINE_WATCH_ONLY) {
|
||||
credit += GetCachableAmount(CREDIT, ISMINE_WATCH_ONLY);
|
||||
}
|
||||
return credit;
|
||||
}
|
||||
|
||||
CAmount CWalletTx::GetDebit(const isminefilter& filter) const
|
||||
{
|
||||
if (tx->vin.empty())
|
||||
return 0;
|
||||
|
||||
CAmount debit = 0;
|
||||
if (filter & ISMINE_SPENDABLE) {
|
||||
debit += GetCachableAmount(DEBIT, ISMINE_SPENDABLE);
|
||||
}
|
||||
if (filter & ISMINE_WATCH_ONLY) {
|
||||
debit += GetCachableAmount(DEBIT, ISMINE_WATCH_ONLY);
|
||||
}
|
||||
return debit;
|
||||
}
|
||||
|
||||
CAmount CWalletTx::GetChange() const
|
||||
{
|
||||
if (fChangeCached)
|
||||
return nChangeCached;
|
||||
nChangeCached = pwallet->GetChange(*tx);
|
||||
fChangeCached = true;
|
||||
return nChangeCached;
|
||||
}
|
||||
|
||||
CAmount CWalletTx::GetImmatureCredit(bool fUseCache) const
|
||||
{
|
||||
if (IsImmatureCoinBase() && IsInMainChain()) {
|
||||
return GetCachableAmount(IMMATURE_CREDIT, ISMINE_SPENDABLE, !fUseCache);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
CAmount CWalletTx::GetImmatureWatchOnlyCredit(const bool fUseCache) const
|
||||
{
|
||||
if (IsImmatureCoinBase() && IsInMainChain()) {
|
||||
return GetCachableAmount(IMMATURE_CREDIT, ISMINE_WATCH_ONLY, !fUseCache);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter) const
|
||||
{
|
||||
if (pwallet == nullptr)
|
||||
return 0;
|
||||
|
||||
// Avoid caching ismine for NO or ALL cases (could remove this check and simplify in the future).
|
||||
bool allow_cache = (filter & ISMINE_ALL) && (filter & ISMINE_ALL) != ISMINE_ALL;
|
||||
|
||||
// Must wait until coinbase is safely deep enough in the chain before valuing it
|
||||
if (IsImmatureCoinBase())
|
||||
return 0;
|
||||
|
||||
if (fUseCache && allow_cache && m_amounts[AVAILABLE_CREDIT].m_cached[filter]) {
|
||||
return m_amounts[AVAILABLE_CREDIT].m_value[filter];
|
||||
}
|
||||
|
||||
bool allow_used_addresses = (filter & ISMINE_USED) || !pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE);
|
||||
CAmount nCredit = 0;
|
||||
uint256 hashTx = GetHash();
|
||||
for (unsigned int i = 0; i < tx->vout.size(); i++)
|
||||
{
|
||||
if (!pwallet->IsSpent(hashTx, i) && (allow_used_addresses || !pwallet->IsSpentKey(hashTx, i))) {
|
||||
const CTxOut &txout = tx->vout[i];
|
||||
nCredit += pwallet->GetCredit(txout, filter);
|
||||
if (!MoneyRange(nCredit))
|
||||
throw std::runtime_error(std::string(__func__) + " : value out of range");
|
||||
}
|
||||
}
|
||||
|
||||
if (allow_cache) {
|
||||
m_amounts[AVAILABLE_CREDIT].Set(filter, nCredit);
|
||||
m_is_cache_empty = false;
|
||||
}
|
||||
|
||||
return nCredit;
|
||||
}
|
||||
|
||||
void CWalletTx::GetAmounts(std::list<COutputEntry>& listReceived,
|
||||
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter) const
|
||||
{
|
||||
nFee = 0;
|
||||
listReceived.clear();
|
||||
listSent.clear();
|
||||
|
||||
// Compute fee:
|
||||
CAmount nDebit = GetDebit(filter);
|
||||
if (nDebit > 0) // debit>0 means we signed/sent this transaction
|
||||
{
|
||||
CAmount nValueOut = tx->GetValueOut();
|
||||
nFee = nDebit - nValueOut;
|
||||
}
|
||||
|
||||
LOCK(pwallet->cs_wallet);
|
||||
// Sent/received.
|
||||
for (unsigned int i = 0; i < tx->vout.size(); ++i)
|
||||
{
|
||||
const CTxOut& txout = tx->vout[i];
|
||||
isminetype fIsMine = pwallet->IsMine(txout);
|
||||
// Only need to handle txouts if AT LEAST one of these is true:
|
||||
// 1) they debit from us (sent)
|
||||
// 2) the output is to us (received)
|
||||
if (nDebit > 0)
|
||||
{
|
||||
// Don't report 'change' txouts
|
||||
if (pwallet->IsChange(txout))
|
||||
continue;
|
||||
}
|
||||
else if (!(fIsMine & filter))
|
||||
continue;
|
||||
|
||||
// In either case, we need to get the destination address
|
||||
CTxDestination address;
|
||||
|
||||
if (!ExtractDestination(txout.scriptPubKey, address) && !txout.scriptPubKey.IsUnspendable())
|
||||
{
|
||||
pwallet->WalletLogPrintf("CWalletTx::GetAmounts: Unknown transaction type found, txid %s\n",
|
||||
this->GetHash().ToString());
|
||||
address = CNoDestination();
|
||||
}
|
||||
|
||||
COutputEntry output = {address, txout.nValue, (int)i};
|
||||
|
||||
// If we are debited by the transaction, add the output as a "sent" entry
|
||||
if (nDebit > 0)
|
||||
listSent.push_back(output);
|
||||
|
||||
// If we are receiving the output, add it as a "received" entry
|
||||
if (fIsMine & filter)
|
||||
listReceived.push_back(output);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool CWallet::IsTrusted(const CWalletTx& wtx, std::set<uint256>& trusted_parents) const
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
// Quick answer in most cases
|
||||
if (!chain().checkFinalTx(*wtx.tx)) return false;
|
||||
int nDepth = wtx.GetDepthInMainChain();
|
||||
if (nDepth >= 1) return true;
|
||||
if (nDepth < 0) return false;
|
||||
// using wtx's cached debit
|
||||
if (!m_spend_zero_conf_change || !wtx.IsFromMe(ISMINE_ALL)) return false;
|
||||
|
||||
// Don't trust unconfirmed transactions from us unless they are in the mempool.
|
||||
if (!wtx.InMempool()) return false;
|
||||
|
||||
// Trusted if all inputs are from us and are in the mempool:
|
||||
for (const CTxIn& txin : wtx.tx->vin)
|
||||
{
|
||||
// Transactions not sent by us: not trusted
|
||||
const CWalletTx* parent = GetWalletTx(txin.prevout.hash);
|
||||
if (parent == nullptr) return false;
|
||||
const CTxOut& parentOut = parent->tx->vout[txin.prevout.n];
|
||||
// Check that this specific input being spent is trusted
|
||||
if (IsMine(parentOut) != ISMINE_SPENDABLE) return false;
|
||||
// If we've already trusted this parent, continue
|
||||
if (trusted_parents.count(parent->GetHash())) continue;
|
||||
// Recurse to check that the parent is also trusted
|
||||
if (!IsTrusted(*parent, trusted_parents)) return false;
|
||||
trusted_parents.insert(parent->GetHash());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CWalletTx::IsTrusted() const
|
||||
{
|
||||
std::set<uint256> trusted_parents;
|
||||
LOCK(pwallet->cs_wallet);
|
||||
return pwallet->IsTrusted(*this, trusted_parents);
|
||||
}
|
||||
|
||||
CWallet::Balance CWallet::GetBalance(const int min_depth, bool avoid_reuse) const
|
||||
{
|
||||
Balance ret;
|
||||
isminefilter reuse_filter = avoid_reuse ? ISMINE_NO : ISMINE_USED;
|
||||
{
|
||||
LOCK(cs_wallet);
|
||||
std::set<uint256> trusted_parents;
|
||||
for (const auto& entry : mapWallet)
|
||||
{
|
||||
const CWalletTx& wtx = entry.second;
|
||||
const bool is_trusted{IsTrusted(wtx, trusted_parents)};
|
||||
const int tx_depth{wtx.GetDepthInMainChain()};
|
||||
const CAmount tx_credit_mine{wtx.GetAvailableCredit(/* fUseCache */ true, ISMINE_SPENDABLE | reuse_filter)};
|
||||
const CAmount tx_credit_watchonly{wtx.GetAvailableCredit(/* fUseCache */ true, ISMINE_WATCH_ONLY | reuse_filter)};
|
||||
if (is_trusted && tx_depth >= min_depth) {
|
||||
ret.m_mine_trusted += tx_credit_mine;
|
||||
ret.m_watchonly_trusted += tx_credit_watchonly;
|
||||
}
|
||||
if (!is_trusted && tx_depth == 0 && wtx.InMempool()) {
|
||||
ret.m_mine_untrusted_pending += tx_credit_mine;
|
||||
ret.m_watchonly_untrusted_pending += tx_credit_watchonly;
|
||||
}
|
||||
ret.m_mine_immature += wtx.GetImmatureCredit();
|
||||
ret.m_watchonly_immature += wtx.GetImmatureWatchOnlyCredit();
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::map<CTxDestination, CAmount> CWallet::GetAddressBalances() const
|
||||
{
|
||||
std::map<CTxDestination, CAmount> balances;
|
||||
|
||||
{
|
||||
LOCK(cs_wallet);
|
||||
std::set<uint256> trusted_parents;
|
||||
for (const auto& walletEntry : mapWallet)
|
||||
{
|
||||
const CWalletTx& wtx = walletEntry.second;
|
||||
|
||||
if (!IsTrusted(wtx, trusted_parents))
|
||||
continue;
|
||||
|
||||
if (wtx.IsImmatureCoinBase())
|
||||
continue;
|
||||
|
||||
int nDepth = wtx.GetDepthInMainChain();
|
||||
if (nDepth < (wtx.IsFromMe(ISMINE_ALL) ? 0 : 1))
|
||||
continue;
|
||||
|
||||
for (unsigned int i = 0; i < wtx.tx->vout.size(); i++)
|
||||
{
|
||||
CTxDestination addr;
|
||||
if (!IsMine(wtx.tx->vout[i]))
|
||||
continue;
|
||||
if(!ExtractDestination(wtx.tx->vout[i].scriptPubKey, addr))
|
||||
continue;
|
||||
|
||||
CAmount n = IsSpent(walletEntry.first, i) ? 0 : wtx.tx->vout[i].nValue;
|
||||
balances[addr] += n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return balances;
|
||||
}
|
||||
|
||||
std::set< std::set<CTxDestination> > CWallet::GetAddressGroupings() const
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
std::set< std::set<CTxDestination> > groupings;
|
||||
std::set<CTxDestination> grouping;
|
||||
|
||||
for (const auto& walletEntry : mapWallet)
|
||||
{
|
||||
const CWalletTx& wtx = walletEntry.second;
|
||||
|
||||
if (wtx.tx->vin.size() > 0)
|
||||
{
|
||||
bool any_mine = false;
|
||||
// group all input addresses with each other
|
||||
for (const CTxIn& txin : wtx.tx->vin)
|
||||
{
|
||||
CTxDestination address;
|
||||
if(!IsMine(txin)) /* If this input isn't mine, ignore it */
|
||||
continue;
|
||||
if(!ExtractDestination(mapWallet.at(txin.prevout.hash).tx->vout[txin.prevout.n].scriptPubKey, address))
|
||||
continue;
|
||||
grouping.insert(address);
|
||||
any_mine = true;
|
||||
}
|
||||
|
||||
// group change with input addresses
|
||||
if (any_mine)
|
||||
{
|
||||
for (const CTxOut& txout : wtx.tx->vout)
|
||||
if (IsChange(txout))
|
||||
{
|
||||
CTxDestination txoutAddr;
|
||||
if(!ExtractDestination(txout.scriptPubKey, txoutAddr))
|
||||
continue;
|
||||
grouping.insert(txoutAddr);
|
||||
}
|
||||
}
|
||||
if (grouping.size() > 0)
|
||||
{
|
||||
groupings.insert(grouping);
|
||||
grouping.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// group lone addrs by themselves
|
||||
for (const auto& txout : wtx.tx->vout)
|
||||
if (IsMine(txout))
|
||||
{
|
||||
CTxDestination address;
|
||||
if(!ExtractDestination(txout.scriptPubKey, address))
|
||||
continue;
|
||||
grouping.insert(address);
|
||||
groupings.insert(grouping);
|
||||
grouping.clear();
|
||||
}
|
||||
}
|
||||
|
||||
std::set< std::set<CTxDestination>* > uniqueGroupings; // a set of pointers to groups of addresses
|
||||
std::map< CTxDestination, std::set<CTxDestination>* > setmap; // map addresses to the unique group containing it
|
||||
for (std::set<CTxDestination> _grouping : groupings)
|
||||
{
|
||||
// make a set of all the groups hit by this new group
|
||||
std::set< std::set<CTxDestination>* > hits;
|
||||
std::map< CTxDestination, std::set<CTxDestination>* >::iterator it;
|
||||
for (const CTxDestination& address : _grouping)
|
||||
if ((it = setmap.find(address)) != setmap.end())
|
||||
hits.insert((*it).second);
|
||||
|
||||
// merge all hit groups into a new single group and delete old groups
|
||||
std::set<CTxDestination>* merged = new std::set<CTxDestination>(_grouping);
|
||||
for (std::set<CTxDestination>* hit : hits)
|
||||
{
|
||||
merged->insert(hit->begin(), hit->end());
|
||||
uniqueGroupings.erase(hit);
|
||||
delete hit;
|
||||
}
|
||||
uniqueGroupings.insert(merged);
|
||||
|
||||
// update setmap
|
||||
for (const CTxDestination& element : *merged)
|
||||
setmap[element] = merged;
|
||||
}
|
||||
|
||||
std::set< std::set<CTxDestination> > ret;
|
||||
for (const std::set<CTxDestination>* uniqueGrouping : uniqueGroupings)
|
||||
{
|
||||
ret.insert(*uniqueGrouping);
|
||||
delete uniqueGrouping;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
20
src/wallet/receive.h
Normal file
20
src/wallet/receive.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) 2021 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_RECEIVE_H
|
||||
#define DIGIBYTE_WALLET_RECEIVE_H
|
||||
|
||||
#include <amount.h>
|
||||
#include <wallet/ismine.h>
|
||||
#include <wallet/transaction.h>
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
struct COutputEntry
|
||||
{
|
||||
CTxDestination destination;
|
||||
CAmount amount;
|
||||
int vout;
|
||||
};
|
||||
|
||||
#endif // DIGIBYTE_WALLET_RECEIVE_H
|
||||
1836
src/wallet/rpcdump.cpp
Normal file
1836
src/wallet/rpcdump.cpp
Normal file
File diff suppressed because it is too large
Load diff
4698
src/wallet/rpcwallet.cpp
Normal file
4698
src/wallet/rpcwallet.cpp
Normal file
File diff suppressed because it is too large
Load diff
41
src/wallet/rpcwallet.h
Normal file
41
src/wallet/rpcwallet.h
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) 2016-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2016-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_RPCWALLET_H
|
||||
#define DIGIBYTE_WALLET_RPCWALLET_H
|
||||
|
||||
#include <span.h>
|
||||
|
||||
#include <any>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class CRPCCommand;
|
||||
class CWallet;
|
||||
class JSONRPCRequest;
|
||||
class LegacyScriptPubKeyMan;
|
||||
class UniValue;
|
||||
class CTransaction;
|
||||
struct PartiallySignedTransaction;
|
||||
struct WalletContext;
|
||||
|
||||
Span<const CRPCCommand> GetWalletRPCCommands();
|
||||
|
||||
/**
|
||||
* Figures out what wallet, if any, to use for a JSONRPCRequest.
|
||||
*
|
||||
* @param[in] request JSONRPCRequest that wishes to access a wallet
|
||||
* @return nullptr if no wallet should be used, or a pointer to the CWallet
|
||||
*/
|
||||
std::shared_ptr<CWallet> GetWalletForJSONRPCRequest(const JSONRPCRequest& request);
|
||||
|
||||
void EnsureWalletIsUnlocked(const CWallet&);
|
||||
WalletContext& EnsureWalletContext(const std::any& context);
|
||||
LegacyScriptPubKeyMan& EnsureLegacyScriptPubKeyMan(CWallet& wallet, bool also_create = false);
|
||||
|
||||
RPCHelpMan getaddressinfo();
|
||||
RPCHelpMan signrawtransactionwithwallet();
|
||||
#endif //DIGIBYTE_WALLET_RPCWALLET_H
|
||||
167
src/wallet/salvage.cpp
Normal file
167
src/wallet/salvage.cpp
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <fs.h>
|
||||
#include <streams.h>
|
||||
#include <util/translation.h>
|
||||
#include <wallet/bdb.h>
|
||||
#include <wallet/salvage.h>
|
||||
#include <wallet/wallet.h>
|
||||
#include <wallet/walletdb.h>
|
||||
|
||||
/* End of headers, beginning of key/value data */
|
||||
static const char *HEADER_END = "HEADER=END";
|
||||
/* End of key/value data */
|
||||
static const char *DATA_END = "DATA=END";
|
||||
typedef std::pair<std::vector<unsigned char>, std::vector<unsigned char> > KeyValPair;
|
||||
|
||||
static bool KeyFilter(const std::string& type)
|
||||
{
|
||||
return WalletBatch::IsKeyType(type) || type == DBKeys::HDCHAIN;
|
||||
}
|
||||
|
||||
bool RecoverDatabaseFile(const fs::path& file_path, bilingual_str& error, std::vector<bilingual_str>& warnings)
|
||||
{
|
||||
DatabaseOptions options;
|
||||
DatabaseStatus status;
|
||||
options.require_existing = true;
|
||||
options.verify = false;
|
||||
options.require_format = DatabaseFormat::BERKELEY;
|
||||
std::unique_ptr<WalletDatabase> database = MakeDatabase(file_path, options, status, error);
|
||||
if (!database) return false;
|
||||
|
||||
BerkeleyDatabase& berkeley_database = static_cast<BerkeleyDatabase&>(*database);
|
||||
std::string filename = berkeley_database.Filename();
|
||||
std::shared_ptr<BerkeleyEnvironment> env = berkeley_database.env;
|
||||
|
||||
if (!env->Open(error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recovery procedure:
|
||||
// move wallet file to walletfilename.timestamp.bak
|
||||
// Call Salvage with fAggressive=true to
|
||||
// get as much data as possible.
|
||||
// Rewrite salvaged data to fresh wallet file
|
||||
// Set -rescan so any missing transactions will be
|
||||
// found.
|
||||
int64_t now = GetTime();
|
||||
std::string newFilename = strprintf("%s.%d.bak", filename, now);
|
||||
|
||||
int result = env->dbenv->dbrename(nullptr, filename.c_str(), nullptr,
|
||||
newFilename.c_str(), DB_AUTO_COMMIT);
|
||||
if (result != 0)
|
||||
{
|
||||
error = strprintf(Untranslated("Failed to rename %s to %s"), filename, newFilename);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Salvage data from a file. The DB_AGGRESSIVE flag is being used (see berkeley DB->verify() method documentation).
|
||||
* key/value pairs are appended to salvagedData which are then written out to a new wallet file.
|
||||
* NOTE: reads the entire database into memory, so cannot be used
|
||||
* for huge databases.
|
||||
*/
|
||||
std::vector<KeyValPair> salvagedData;
|
||||
|
||||
std::stringstream strDump;
|
||||
|
||||
Db db(env->dbenv.get(), 0);
|
||||
result = db.verify(newFilename.c_str(), nullptr, &strDump, DB_SALVAGE | DB_AGGRESSIVE);
|
||||
if (result == DB_VERIFY_BAD) {
|
||||
warnings.push_back(Untranslated("Salvage: Database salvage found errors, all data may not be recoverable."));
|
||||
}
|
||||
if (result != 0 && result != DB_VERIFY_BAD) {
|
||||
error = strprintf(Untranslated("Salvage: Database salvage failed with result %d."), result);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Format of bdb dump is ascii lines:
|
||||
// header lines...
|
||||
// HEADER=END
|
||||
// hexadecimal key
|
||||
// hexadecimal value
|
||||
// ... repeated
|
||||
// DATA=END
|
||||
|
||||
std::string strLine;
|
||||
while (!strDump.eof() && strLine != HEADER_END)
|
||||
getline(strDump, strLine); // Skip past header
|
||||
|
||||
std::string keyHex, valueHex;
|
||||
while (!strDump.eof() && keyHex != DATA_END) {
|
||||
getline(strDump, keyHex);
|
||||
if (keyHex != DATA_END) {
|
||||
if (strDump.eof())
|
||||
break;
|
||||
getline(strDump, valueHex);
|
||||
if (valueHex == DATA_END) {
|
||||
warnings.push_back(Untranslated("Salvage: WARNING: Number of keys in data does not match number of values."));
|
||||
break;
|
||||
}
|
||||
salvagedData.push_back(make_pair(ParseHex(keyHex), ParseHex(valueHex)));
|
||||
}
|
||||
}
|
||||
|
||||
bool fSuccess;
|
||||
if (keyHex != DATA_END) {
|
||||
warnings.push_back(Untranslated("Salvage: WARNING: Unexpected end of file while reading salvage output."));
|
||||
fSuccess = false;
|
||||
} else {
|
||||
fSuccess = (result == 0);
|
||||
}
|
||||
|
||||
if (salvagedData.empty())
|
||||
{
|
||||
error = strprintf(Untranslated("Salvage(aggressive) found no records in %s."), newFilename);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<Db> pdbCopy = std::make_unique<Db>(env->dbenv.get(), 0);
|
||||
int ret = pdbCopy->open(nullptr, // Txn pointer
|
||||
filename.c_str(), // Filename
|
||||
"main", // Logical db name
|
||||
DB_BTREE, // Database type
|
||||
DB_CREATE, // Flags
|
||||
0);
|
||||
if (ret > 0) {
|
||||
error = strprintf(Untranslated("Cannot create database file %s"), filename);
|
||||
pdbCopy->close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
DbTxn* ptxn = env->TxnBegin();
|
||||
CWallet dummyWallet(nullptr, "", CreateDummyWalletDatabase());
|
||||
for (KeyValPair& row : salvagedData)
|
||||
{
|
||||
/* Filter for only private key type KV pairs to be added to the salvaged wallet */
|
||||
CDataStream ssKey(row.first, SER_DISK, CLIENT_VERSION);
|
||||
CDataStream ssValue(row.second, SER_DISK, CLIENT_VERSION);
|
||||
std::string strType, strErr;
|
||||
bool fReadOK;
|
||||
{
|
||||
// Required in LoadKeyMetadata():
|
||||
LOCK(dummyWallet.cs_wallet);
|
||||
fReadOK = ReadKeyValue(&dummyWallet, ssKey, ssValue, strType, strErr, KeyFilter);
|
||||
}
|
||||
if (!KeyFilter(strType)) {
|
||||
continue;
|
||||
}
|
||||
if (!fReadOK)
|
||||
{
|
||||
warnings.push_back(strprintf(Untranslated("WARNING: WalletBatch::Recover skipping %s: %s"), strType, strErr));
|
||||
continue;
|
||||
}
|
||||
Dbt datKey(row.first.data(), row.first.size());
|
||||
Dbt datValue(row.second.data(), row.second.size());
|
||||
int ret2 = pdbCopy->put(ptxn, &datKey, &datValue, DB_NOOVERWRITE);
|
||||
if (ret2 > 0)
|
||||
fSuccess = false;
|
||||
}
|
||||
ptxn->commit(0);
|
||||
pdbCopy->close(0);
|
||||
|
||||
return fSuccess;
|
||||
}
|
||||
16
src/wallet/salvage.h
Normal file
16
src/wallet/salvage.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_SALVAGE_H
|
||||
#define DIGIBYTE_WALLET_SALVAGE_H
|
||||
|
||||
#include <fs.h>
|
||||
#include <streams.h>
|
||||
|
||||
struct bilingual_str;
|
||||
|
||||
bool RecoverDatabaseFile(const fs::path& file_path, bilingual_str& error, std::vector<bilingual_str>& warnings);
|
||||
|
||||
#endif // DIGIBYTE_WALLET_SALVAGE_H
|
||||
2339
src/wallet/scriptpubkeyman.cpp
Normal file
2339
src/wallet/scriptpubkeyman.cpp
Normal file
File diff suppressed because it is too large
Load diff
629
src/wallet/scriptpubkeyman.h
Normal file
629
src/wallet/scriptpubkeyman.h
Normal file
|
|
@ -0,0 +1,629 @@
|
|||
// Copyright (c) 2019-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_SCRIPTPUBKEYMAN_H
|
||||
#define DIGIBYTE_WALLET_SCRIPTPUBKEYMAN_H
|
||||
|
||||
#include <psbt.h>
|
||||
#include <script/descriptor.h>
|
||||
#include <script/signingprovider.h>
|
||||
#include <script/standard.h>
|
||||
#include <util/error.h>
|
||||
#include <util/message.h>
|
||||
#include <util/time.h>
|
||||
#include <wallet/crypter.h>
|
||||
#include <wallet/ismine.h>
|
||||
#include <wallet/walletdb.h>
|
||||
#include <wallet/walletutil.h>
|
||||
|
||||
#include <boost/signals2/signal.hpp>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
enum class OutputType;
|
||||
struct bilingual_str;
|
||||
|
||||
// Wallet storage things that ScriptPubKeyMans need in order to be able to store things to the wallet database.
|
||||
// It provides access to things that are part of the entire wallet and not specific to a ScriptPubKeyMan such as
|
||||
// wallet flags, wallet version, encryption keys, encryption status, and the database itself. This allows a
|
||||
// ScriptPubKeyMan to have callbacks into CWallet without causing a circular dependency.
|
||||
// WalletStorage should be the same for all ScriptPubKeyMans of a wallet.
|
||||
class WalletStorage
|
||||
{
|
||||
public:
|
||||
virtual ~WalletStorage() = default;
|
||||
virtual const std::string GetDisplayName() const = 0;
|
||||
virtual WalletDatabase& GetDatabase() const = 0;
|
||||
virtual bool IsWalletFlagSet(uint64_t) const = 0;
|
||||
virtual void UnsetBlankWalletFlag(WalletBatch&) = 0;
|
||||
virtual bool CanSupportFeature(enum WalletFeature) const = 0;
|
||||
virtual void SetMinVersion(enum WalletFeature, WalletBatch* = nullptr) = 0;
|
||||
virtual const CKeyingMaterial& GetEncryptionKey() const = 0;
|
||||
virtual bool HasEncryptionKeys() const = 0;
|
||||
virtual bool IsLocked() const = 0;
|
||||
};
|
||||
|
||||
//! Default for -keypool
|
||||
static const unsigned int DEFAULT_KEYPOOL_SIZE = 1000;
|
||||
|
||||
std::vector<CKeyID> GetAffectedKeys(const CScript& spk, const SigningProvider& provider);
|
||||
|
||||
/** A key from a CWallet's keypool
|
||||
*
|
||||
* The wallet holds one (for pre HD-split wallets) or several keypools. These
|
||||
* are sets of keys that have not yet been used to provide addresses or receive
|
||||
* change.
|
||||
*
|
||||
* The Vexta Core wallet was originally a collection of unrelated private
|
||||
* keys with their associated addresses. If a non-HD wallet generated a
|
||||
* key/address, gave that address out and then restored a backup from before
|
||||
* that key's generation, then any funds sent to that address would be
|
||||
* lost definitively.
|
||||
*
|
||||
* The keypool was implemented to avoid this scenario (commit: 10384941). The
|
||||
* wallet would generate a set of keys (100 by default). When a new public key
|
||||
* was required, either to give out as an address or to use in a change output,
|
||||
* it would be drawn from the keypool. The keypool would then be topped up to
|
||||
* maintain 100 keys. This ensured that as long as the wallet hadn't used more
|
||||
* than 100 keys since the previous backup, all funds would be safe, since a
|
||||
* restored wallet would be able to scan for all owned addresses.
|
||||
*
|
||||
* A keypool also allowed encrypted wallets to give out addresses without
|
||||
* having to be decrypted to generate a new private key.
|
||||
*
|
||||
* With the introduction of HD wallets (commit: f1902510), the keypool
|
||||
* essentially became an address look-ahead pool. Restoring old backups can no
|
||||
* longer definitively lose funds as long as the addresses used were from the
|
||||
* wallet's HD seed (since all private keys can be rederived from the seed).
|
||||
* However, if many addresses were used since the backup, then the wallet may
|
||||
* not know how far ahead in the HD chain to look for its addresses. The
|
||||
* keypool is used to implement a 'gap limit'. The keypool maintains a set of
|
||||
* keys (by default 1000) ahead of the last used key and scans for the
|
||||
* addresses of those keys. This avoids the risk of not seeing transactions
|
||||
* involving the wallet's addresses, or of re-using the same address.
|
||||
* In the unlikely case where none of the addresses in the `gap limit` are
|
||||
* used on-chain, the look-ahead will not be incremented to keep
|
||||
* a constant size and addresses beyond this range will not be detected by an
|
||||
* old backup. For this reason, it is not recommended to decrease keypool size
|
||||
* lower than default value.
|
||||
*
|
||||
* The HD-split wallet feature added a second keypool (commit: 02592f4c). There
|
||||
* is an external keypool (for addresses to hand out) and an internal keypool
|
||||
* (for change addresses).
|
||||
*
|
||||
* Keypool keys are stored in the wallet/keystore's keymap. The keypool data is
|
||||
* stored as sets of indexes in the wallet (setInternalKeyPool,
|
||||
* setExternalKeyPool and set_pre_split_keypool), and a map from the key to the
|
||||
* index (m_pool_key_to_index). The CKeyPool object is used to
|
||||
* serialize/deserialize the pool data to/from the database.
|
||||
*/
|
||||
class CKeyPool
|
||||
{
|
||||
public:
|
||||
//! The time at which the key was generated. Set in AddKeypoolPubKeyWithDB
|
||||
int64_t nTime;
|
||||
//! The public key
|
||||
CPubKey vchPubKey;
|
||||
//! Whether this keypool entry is in the internal keypool (for change outputs)
|
||||
bool fInternal;
|
||||
//! Whether this key was generated for a keypool before the wallet was upgraded to HD-split
|
||||
bool m_pre_split;
|
||||
|
||||
CKeyPool();
|
||||
CKeyPool(const CPubKey& vchPubKeyIn, bool internalIn);
|
||||
|
||||
template<typename Stream>
|
||||
void Serialize(Stream& s) const
|
||||
{
|
||||
int nVersion = s.GetVersion();
|
||||
if (!(s.GetType() & SER_GETHASH)) {
|
||||
s << nVersion;
|
||||
}
|
||||
s << nTime << vchPubKey << fInternal << m_pre_split;
|
||||
}
|
||||
|
||||
template<typename Stream>
|
||||
void Unserialize(Stream& s)
|
||||
{
|
||||
int nVersion = s.GetVersion();
|
||||
if (!(s.GetType() & SER_GETHASH)) {
|
||||
s >> nVersion;
|
||||
}
|
||||
s >> nTime >> vchPubKey;
|
||||
try {
|
||||
s >> fInternal;
|
||||
} catch (std::ios_base::failure&) {
|
||||
/* flag as external address if we can't read the internal boolean
|
||||
(this will be the case for any wallet before the HD chain split version) */
|
||||
fInternal = false;
|
||||
}
|
||||
try {
|
||||
s >> m_pre_split;
|
||||
} catch (std::ios_base::failure&) {
|
||||
/* flag as postsplit address if we can't read the m_pre_split boolean
|
||||
(this will be the case for any wallet that upgrades to HD chain split) */
|
||||
m_pre_split = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class KeyIDHasher
|
||||
{
|
||||
public:
|
||||
KeyIDHasher() {}
|
||||
|
||||
size_t operator()(const CKeyID& id) const
|
||||
{
|
||||
return id.GetUint64(0);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* A class implementing ScriptPubKeyMan manages some (or all) scriptPubKeys used in a wallet.
|
||||
* It contains the scripts and keys related to the scriptPubKeys it manages.
|
||||
* A ScriptPubKeyMan will be able to give out scriptPubKeys to be used, as well as marking
|
||||
* when a scriptPubKey has been used. It also handles when and how to store a scriptPubKey
|
||||
* and its related scripts and keys, including encryption.
|
||||
*/
|
||||
class ScriptPubKeyMan
|
||||
{
|
||||
protected:
|
||||
WalletStorage& m_storage;
|
||||
|
||||
public:
|
||||
explicit ScriptPubKeyMan(WalletStorage& storage) : m_storage(storage) {}
|
||||
virtual ~ScriptPubKeyMan() {};
|
||||
virtual bool GetNewDestination(const OutputType type, CTxDestination& dest, std::string& error) { return false; }
|
||||
virtual isminetype IsMine(const CScript& script) const { return ISMINE_NO; }
|
||||
|
||||
//! Check that the given decryption key is valid for this ScriptPubKeyMan, i.e. it decrypts all of the keys handled by it.
|
||||
virtual bool CheckDecryptionKey(const CKeyingMaterial& master_key, bool accept_no_keys = false) { return false; }
|
||||
virtual bool Encrypt(const CKeyingMaterial& master_key, WalletBatch* batch) { return false; }
|
||||
|
||||
virtual bool GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool, std::string& error) { return false; }
|
||||
virtual void KeepDestination(int64_t index, const OutputType& type) {}
|
||||
virtual void ReturnDestination(int64_t index, bool internal, const CTxDestination& addr) {}
|
||||
|
||||
/** Fills internal address pool. Use within ScriptPubKeyMan implementations should be used sparingly and only
|
||||
* when something from the address pool is removed, excluding GetNewDestination and GetReservedDestination.
|
||||
* External wallet code is primarily responsible for topping up prior to fetching new addresses
|
||||
*/
|
||||
virtual bool TopUp(unsigned int size = 0) { return false; }
|
||||
|
||||
//! Mark unused addresses as being used
|
||||
virtual void MarkUnusedAddresses(const CScript& script) {}
|
||||
|
||||
/** Sets up the key generation stuff, i.e. generates new HD seeds and sets them as active.
|
||||
* Returns false if already setup or setup fails, true if setup is successful
|
||||
* Set force=true to make it re-setup if already setup, used for upgrades
|
||||
*/
|
||||
virtual bool SetupGeneration(bool force = false) { return false; }
|
||||
|
||||
/* Returns true if HD is enabled */
|
||||
virtual bool IsHDEnabled() const { return false; }
|
||||
|
||||
/* Returns true if the wallet can give out new addresses. This means it has keys in the keypool or can generate new keys */
|
||||
virtual bool CanGetAddresses(bool internal = false) const { return false; }
|
||||
|
||||
/** Upgrades the wallet to the specified version */
|
||||
virtual bool Upgrade(int prev_version, int new_version, bilingual_str& error) { return true; }
|
||||
|
||||
virtual bool HavePrivateKeys() const { return false; }
|
||||
|
||||
//! The action to do when the DB needs rewrite
|
||||
virtual void RewriteDB() {}
|
||||
|
||||
virtual int64_t GetOldestKeyPoolTime() const { return GetTime(); }
|
||||
|
||||
virtual unsigned int GetKeyPoolSize() const { return 0; }
|
||||
|
||||
virtual int64_t GetTimeFirstKey() const { return 0; }
|
||||
|
||||
virtual std::unique_ptr<CKeyMetadata> GetMetadata(const CTxDestination& dest) const { return nullptr; }
|
||||
|
||||
virtual std::unique_ptr<SigningProvider> GetSolvingProvider(const CScript& script) const { return nullptr; }
|
||||
|
||||
/** Whether this ScriptPubKeyMan can provide a SigningProvider (via GetSolvingProvider) that, combined with
|
||||
* sigdata, can produce solving data.
|
||||
*/
|
||||
virtual bool CanProvide(const CScript& script, SignatureData& sigdata) { return false; }
|
||||
|
||||
/** Creates new signatures and adds them to the transaction. Returns whether all inputs were signed */
|
||||
virtual bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const { return false; }
|
||||
/** Sign a message with the given script */
|
||||
virtual SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const { return SigningResult::SIGNING_FAILED; };
|
||||
/** Adds script and derivation path information to a PSBT, and optionally signs it. */
|
||||
virtual TransactionError FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const { return TransactionError::INVALID_PSBT; }
|
||||
|
||||
virtual uint256 GetID() const { return uint256(); }
|
||||
|
||||
/** Prepends the wallet name in logging output to ease debugging in multi-wallet use cases */
|
||||
template<typename... Params>
|
||||
void WalletLogPrintf(std::string fmt, Params... parameters) const {
|
||||
LogPrintf(("%s " + fmt).c_str(), m_storage.GetDisplayName(), parameters...);
|
||||
};
|
||||
|
||||
/** Watch-only address added */
|
||||
boost::signals2::signal<void (bool fHaveWatchOnly)> NotifyWatchonlyChanged;
|
||||
|
||||
/** Keypool has new keys */
|
||||
boost::signals2::signal<void ()> NotifyCanGetAddressesChanged;
|
||||
};
|
||||
|
||||
/** OutputTypes supported by the LegacyScriptPubKeyMan */
|
||||
static const std::unordered_set<OutputType> LEGACY_OUTPUT_TYPES {
|
||||
OutputType::LEGACY,
|
||||
OutputType::P2SH_SEGWIT,
|
||||
OutputType::BECH32,
|
||||
};
|
||||
|
||||
class LegacyScriptPubKeyMan : public ScriptPubKeyMan, public FillableSigningProvider
|
||||
{
|
||||
private:
|
||||
//! keeps track of whether Unlock has run a thorough check before
|
||||
bool fDecryptionThoroughlyChecked = true;
|
||||
|
||||
using WatchOnlySet = std::set<CScript>;
|
||||
using WatchKeyMap = std::map<CKeyID, CPubKey>;
|
||||
|
||||
WalletBatch *encrypted_batch GUARDED_BY(cs_KeyStore) = nullptr;
|
||||
|
||||
using CryptedKeyMap = std::map<CKeyID, std::pair<CPubKey, std::vector<unsigned char>>>;
|
||||
|
||||
CryptedKeyMap mapCryptedKeys GUARDED_BY(cs_KeyStore);
|
||||
WatchOnlySet setWatchOnly GUARDED_BY(cs_KeyStore);
|
||||
WatchKeyMap mapWatchKeys GUARDED_BY(cs_KeyStore);
|
||||
|
||||
int64_t nTimeFirstKey GUARDED_BY(cs_KeyStore) = 0;
|
||||
|
||||
bool AddKeyPubKeyInner(const CKey& key, const CPubKey &pubkey);
|
||||
bool AddCryptedKeyInner(const CPubKey &vchPubKey, const std::vector<unsigned char> &vchCryptedSecret);
|
||||
|
||||
/**
|
||||
* Private version of AddWatchOnly method which does not accept a
|
||||
* timestamp, and which will reset the wallet's nTimeFirstKey value to 1 if
|
||||
* the watch key did not previously have a timestamp associated with it.
|
||||
* Because this is an inherited virtual method, it is accessible despite
|
||||
* being marked private, but it is marked private anyway to encourage use
|
||||
* of the other AddWatchOnly which accepts a timestamp and sets
|
||||
* nTimeFirstKey more intelligently for more efficient rescans.
|
||||
*/
|
||||
bool AddWatchOnly(const CScript& dest) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
|
||||
bool AddWatchOnlyWithDB(WalletBatch &batch, const CScript& dest) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
|
||||
bool AddWatchOnlyInMem(const CScript &dest);
|
||||
//! Adds a watch-only address to the store, and saves it to disk.
|
||||
bool AddWatchOnlyWithDB(WalletBatch &batch, const CScript& dest, int64_t create_time) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
|
||||
|
||||
//! Adds a key to the store, and saves it to disk.
|
||||
bool AddKeyPubKeyWithDB(WalletBatch &batch,const CKey& key, const CPubKey &pubkey) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
|
||||
|
||||
void AddKeypoolPubkeyWithDB(const CPubKey& pubkey, const bool internal, WalletBatch& batch);
|
||||
|
||||
//! Adds a script to the store and saves it to disk
|
||||
bool AddCScriptWithDB(WalletBatch& batch, const CScript& script);
|
||||
|
||||
/** Add a KeyOriginInfo to the wallet */
|
||||
bool AddKeyOriginWithDB(WalletBatch& batch, const CPubKey& pubkey, const KeyOriginInfo& info);
|
||||
|
||||
/* the HD chain data model (external chain counters) */
|
||||
CHDChain m_hd_chain;
|
||||
std::unordered_map<CKeyID, CHDChain, SaltedSipHasher> m_inactive_hd_chains;
|
||||
|
||||
/* HD derive new child key (on internal or external chain) */
|
||||
void DeriveNewChildKey(WalletBatch& batch, CKeyMetadata& metadata, CKey& secret, CHDChain& hd_chain, bool internal = false) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
|
||||
|
||||
std::set<int64_t> setInternalKeyPool GUARDED_BY(cs_KeyStore);
|
||||
std::set<int64_t> setExternalKeyPool GUARDED_BY(cs_KeyStore);
|
||||
std::set<int64_t> set_pre_split_keypool GUARDED_BY(cs_KeyStore);
|
||||
int64_t m_max_keypool_index GUARDED_BY(cs_KeyStore) = 0;
|
||||
std::map<CKeyID, int64_t> m_pool_key_to_index;
|
||||
// Tracks keypool indexes to CKeyIDs of keys that have been taken out of the keypool but may be returned to it
|
||||
std::map<int64_t, CKeyID> m_index_to_reserved_key;
|
||||
|
||||
//! Fetches a key from the keypool
|
||||
bool GetKeyFromPool(CPubKey &key, const OutputType type, bool internal = false);
|
||||
|
||||
/**
|
||||
* Reserves a key from the keypool and sets nIndex to its index
|
||||
*
|
||||
* @param[out] nIndex the index of the key in keypool
|
||||
* @param[out] keypool the keypool the key was drawn from, which could be the
|
||||
* the pre-split pool if present, or the internal or external pool
|
||||
* @param fRequestedInternal true if the caller would like the key drawn
|
||||
* from the internal keypool, false if external is preferred
|
||||
*
|
||||
* @return true if succeeded, false if failed due to empty keypool
|
||||
* @throws std::runtime_error if keypool read failed, key was invalid,
|
||||
* was not found in the wallet, or was misclassified in the internal
|
||||
* or external keypool
|
||||
*/
|
||||
bool ReserveKeyFromKeyPool(int64_t& nIndex, CKeyPool& keypool, bool fRequestedInternal);
|
||||
|
||||
/**
|
||||
* Like TopUp() but adds keys for inactive HD chains.
|
||||
* Ensures that there are at least -keypool number of keys derived after the given index.
|
||||
*
|
||||
* @param seed_id the CKeyID for the HD seed.
|
||||
* @param index the index to start generating keys from
|
||||
* @param internal whether the internal chain should be used. true for internal chain, false for external chain.
|
||||
*
|
||||
* @return true if seed was found and keys were derived. false if unable to derive seeds
|
||||
*/
|
||||
bool TopUpInactiveHDChain(const CKeyID seed_id, int64_t index, bool internal);
|
||||
|
||||
public:
|
||||
using ScriptPubKeyMan::ScriptPubKeyMan;
|
||||
|
||||
bool GetNewDestination(const OutputType type, CTxDestination& dest, std::string& error) override;
|
||||
isminetype IsMine(const CScript& script) const override;
|
||||
|
||||
bool CheckDecryptionKey(const CKeyingMaterial& master_key, bool accept_no_keys = false) override;
|
||||
bool Encrypt(const CKeyingMaterial& master_key, WalletBatch* batch) override;
|
||||
|
||||
bool GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool, std::string& error) override;
|
||||
void KeepDestination(int64_t index, const OutputType& type) override;
|
||||
void ReturnDestination(int64_t index, bool internal, const CTxDestination&) override;
|
||||
|
||||
bool TopUp(unsigned int size = 0) override;
|
||||
|
||||
void MarkUnusedAddresses(const CScript& script) override;
|
||||
|
||||
//! Upgrade stored CKeyMetadata objects to store key origin info as KeyOriginInfo
|
||||
void UpgradeKeyMetadata();
|
||||
|
||||
bool IsHDEnabled() const override;
|
||||
|
||||
bool SetupGeneration(bool force = false) override;
|
||||
|
||||
bool Upgrade(int prev_version, int new_version, bilingual_str& error) override;
|
||||
|
||||
bool HavePrivateKeys() const override;
|
||||
|
||||
void RewriteDB() override;
|
||||
|
||||
int64_t GetOldestKeyPoolTime() const override;
|
||||
size_t KeypoolCountExternalKeys() const;
|
||||
unsigned int GetKeyPoolSize() const override;
|
||||
|
||||
int64_t GetTimeFirstKey() const override;
|
||||
|
||||
std::unique_ptr<CKeyMetadata> GetMetadata(const CTxDestination& dest) const override;
|
||||
|
||||
bool CanGetAddresses(bool internal = false) const override;
|
||||
|
||||
std::unique_ptr<SigningProvider> GetSolvingProvider(const CScript& script) const override;
|
||||
|
||||
bool CanProvide(const CScript& script, SignatureData& sigdata) override;
|
||||
|
||||
bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const override;
|
||||
SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const override;
|
||||
TransactionError FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override;
|
||||
|
||||
uint256 GetID() const override;
|
||||
|
||||
// Map from Key ID to key metadata.
|
||||
std::map<CKeyID, CKeyMetadata> mapKeyMetadata GUARDED_BY(cs_KeyStore);
|
||||
|
||||
// Map from Script ID to key metadata (for watch-only keys).
|
||||
std::map<CScriptID, CKeyMetadata> m_script_metadata GUARDED_BY(cs_KeyStore);
|
||||
|
||||
//! Adds a key to the store, and saves it to disk.
|
||||
bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey) override;
|
||||
//! Adds a key to the store, without saving it to disk (used by LoadWallet)
|
||||
bool LoadKey(const CKey& key, const CPubKey &pubkey);
|
||||
//! Adds an encrypted key to the store, and saves it to disk.
|
||||
bool AddCryptedKey(const CPubKey &vchPubKey, const std::vector<unsigned char> &vchCryptedSecret);
|
||||
//! Adds an encrypted key to the store, without saving it to disk (used by LoadWallet)
|
||||
bool LoadCryptedKey(const CPubKey &vchPubKey, const std::vector<unsigned char> &vchCryptedSecret, bool checksum_valid);
|
||||
void UpdateTimeFirstKey(int64_t nCreateTime) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
|
||||
//! Adds a CScript to the store
|
||||
bool LoadCScript(const CScript& redeemScript);
|
||||
//! Load metadata (used by LoadWallet)
|
||||
void LoadKeyMetadata(const CKeyID& keyID, const CKeyMetadata &metadata);
|
||||
void LoadScriptMetadata(const CScriptID& script_id, const CKeyMetadata &metadata);
|
||||
//! Generate a new key
|
||||
CPubKey GenerateNewKey(WalletBatch& batch, CHDChain& hd_chain, bool internal = false) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
|
||||
|
||||
/* Set the HD chain model (chain child index counters) and writes it to the database */
|
||||
void AddHDChain(const CHDChain& chain);
|
||||
//! Load a HD chain model (used by LoadWallet)
|
||||
void LoadHDChain(const CHDChain& chain);
|
||||
const CHDChain& GetHDChain() const { return m_hd_chain; }
|
||||
void AddInactiveHDChain(const CHDChain& chain);
|
||||
|
||||
//! Adds a watch-only address to the store, without saving it to disk (used by LoadWallet)
|
||||
bool LoadWatchOnly(const CScript &dest);
|
||||
//! Returns whether the watch-only script is in the wallet
|
||||
bool HaveWatchOnly(const CScript &dest) const;
|
||||
//! Returns whether there are any watch-only things in the wallet
|
||||
bool HaveWatchOnly() const;
|
||||
//! Remove a watch only script from the keystore
|
||||
bool RemoveWatchOnly(const CScript &dest);
|
||||
bool AddWatchOnly(const CScript& dest, int64_t nCreateTime) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
|
||||
|
||||
//! Fetches a pubkey from mapWatchKeys if it exists there
|
||||
bool GetWatchPubKey(const CKeyID &address, CPubKey &pubkey_out) const;
|
||||
|
||||
/* SigningProvider overrides */
|
||||
bool HaveKey(const CKeyID &address) const override;
|
||||
bool GetKey(const CKeyID &address, CKey& keyOut) const override;
|
||||
bool GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) const override;
|
||||
bool AddCScript(const CScript& redeemScript) override;
|
||||
bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const override;
|
||||
|
||||
//! Load a keypool entry
|
||||
void LoadKeyPool(int64_t nIndex, const CKeyPool &keypool);
|
||||
bool NewKeyPool();
|
||||
void MarkPreSplitKeys() EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
|
||||
|
||||
bool ImportScripts(const std::set<CScript> scripts, int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
|
||||
bool ImportPrivKeys(const std::map<CKeyID, CKey>& privkey_map, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
|
||||
bool ImportPubKeys(const std::vector<CKeyID>& ordered_pubkeys, const std::map<CKeyID, CPubKey>& pubkey_map, const std::map<CKeyID, std::pair<CPubKey, KeyOriginInfo>>& key_origins, const bool add_keypool, const bool internal, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
|
||||
bool ImportScriptPubKeys(const std::set<CScript>& script_pub_keys, const bool have_solving_data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
|
||||
|
||||
/* Returns true if the wallet can generate new keys */
|
||||
bool CanGenerateKeys() const;
|
||||
|
||||
/* Generates a new HD seed (will not be activated) */
|
||||
CPubKey GenerateNewSeed();
|
||||
|
||||
/* Derives a new HD seed (will not be activated) */
|
||||
CPubKey DeriveNewSeed(const CKey& key);
|
||||
|
||||
/* Set the current HD seed (will reset the chain child index counters)
|
||||
Sets the seed's version based on the current wallet version (so the
|
||||
caller must ensure the current wallet version is correct before calling
|
||||
this function). */
|
||||
void SetHDSeed(const CPubKey& key);
|
||||
|
||||
/**
|
||||
* Explicitly make the wallet learn the related scripts for outputs to the
|
||||
* given key. This is purely to make the wallet file compatible with older
|
||||
* software, as FillableSigningProvider automatically does this implicitly for all
|
||||
* keys now.
|
||||
*/
|
||||
void LearnRelatedScripts(const CPubKey& key, OutputType);
|
||||
|
||||
/**
|
||||
* Same as LearnRelatedScripts, but when the OutputType is not known (and could
|
||||
* be anything).
|
||||
*/
|
||||
void LearnAllRelatedScripts(const CPubKey& key);
|
||||
|
||||
/**
|
||||
* Marks all keys in the keypool up to and including reserve_key as used.
|
||||
*/
|
||||
void MarkReserveKeysAsUsed(int64_t keypool_id) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
|
||||
const std::map<CKeyID, int64_t>& GetAllReserveKeys() const { return m_pool_key_to_index; }
|
||||
|
||||
std::set<CKeyID> GetKeys() const override;
|
||||
};
|
||||
|
||||
/** Wraps a LegacyScriptPubKeyMan so that it can be returned in a new unique_ptr. Does not provide privkeys */
|
||||
class LegacySigningProvider : public SigningProvider
|
||||
{
|
||||
private:
|
||||
const LegacyScriptPubKeyMan& m_spk_man;
|
||||
public:
|
||||
explicit LegacySigningProvider(const LegacyScriptPubKeyMan& spk_man) : m_spk_man(spk_man) {}
|
||||
|
||||
bool GetCScript(const CScriptID &scriptid, CScript& script) const override { return m_spk_man.GetCScript(scriptid, script); }
|
||||
bool HaveCScript(const CScriptID &scriptid) const override { return m_spk_man.HaveCScript(scriptid); }
|
||||
bool GetPubKey(const CKeyID &address, CPubKey& pubkey) const override { return m_spk_man.GetPubKey(address, pubkey); }
|
||||
bool GetKey(const CKeyID &address, CKey& key) const override { return false; }
|
||||
bool HaveKey(const CKeyID &address) const override { return false; }
|
||||
bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const override { return m_spk_man.GetKeyOrigin(keyid, info); }
|
||||
};
|
||||
|
||||
class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
|
||||
{
|
||||
private:
|
||||
using ScriptPubKeyMap = std::map<CScript, int32_t>; // Map of scripts to descriptor range index
|
||||
using PubKeyMap = std::map<CPubKey, int32_t>; // Map of pubkeys involved in scripts to descriptor range index
|
||||
using CryptedKeyMap = std::map<CKeyID, std::pair<CPubKey, std::vector<unsigned char>>>;
|
||||
using KeyMap = std::map<CKeyID, CKey>;
|
||||
|
||||
ScriptPubKeyMap m_map_script_pub_keys GUARDED_BY(cs_desc_man);
|
||||
PubKeyMap m_map_pubkeys GUARDED_BY(cs_desc_man);
|
||||
int32_t m_max_cached_index = -1;
|
||||
|
||||
KeyMap m_map_keys GUARDED_BY(cs_desc_man);
|
||||
CryptedKeyMap m_map_crypted_keys GUARDED_BY(cs_desc_man);
|
||||
|
||||
//! keeps track of whether Unlock has run a thorough check before
|
||||
bool m_decryption_thoroughly_checked = false;
|
||||
|
||||
bool AddDescriptorKeyWithDB(WalletBatch& batch, const CKey& key, const CPubKey &pubkey) EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
|
||||
|
||||
KeyMap GetKeys() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
|
||||
|
||||
// Fetch the SigningProvider for the given script and optionally include private keys
|
||||
std::unique_ptr<FlatSigningProvider> GetSigningProvider(const CScript& script, bool include_private = false) const;
|
||||
// Fetch the SigningProvider for the given pubkey and always include private keys. This should only be called by signing code.
|
||||
std::unique_ptr<FlatSigningProvider> GetSigningProvider(const CPubKey& pubkey) const;
|
||||
// Fetch the SigningProvider for a given index and optionally include private keys. Called by the above functions.
|
||||
std::unique_ptr<FlatSigningProvider> GetSigningProvider(int32_t index, bool include_private = false) const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
|
||||
|
||||
protected:
|
||||
WalletDescriptor m_wallet_descriptor GUARDED_BY(cs_desc_man);
|
||||
|
||||
public:
|
||||
DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor)
|
||||
: ScriptPubKeyMan(storage),
|
||||
m_wallet_descriptor(descriptor)
|
||||
{}
|
||||
DescriptorScriptPubKeyMan(WalletStorage& storage)
|
||||
: ScriptPubKeyMan(storage)
|
||||
{}
|
||||
|
||||
mutable RecursiveMutex cs_desc_man;
|
||||
|
||||
bool GetNewDestination(const OutputType type, CTxDestination& dest, std::string& error) override;
|
||||
isminetype IsMine(const CScript& script) const override;
|
||||
|
||||
bool CheckDecryptionKey(const CKeyingMaterial& master_key, bool accept_no_keys = false) override;
|
||||
bool Encrypt(const CKeyingMaterial& master_key, WalletBatch* batch) override;
|
||||
|
||||
bool GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool, std::string& error) override;
|
||||
void ReturnDestination(int64_t index, bool internal, const CTxDestination& addr) override;
|
||||
|
||||
// Tops up the descriptor cache and m_map_script_pub_keys. The cache is stored in the wallet file
|
||||
// and is used to expand the descriptor in GetNewDestination. DescriptorScriptPubKeyMan relies
|
||||
// more on ephemeral data than LegacyScriptPubKeyMan. For wallets using unhardened derivation
|
||||
// (with or without private keys), the "keypool" is a single xpub.
|
||||
bool TopUp(unsigned int size = 0) override;
|
||||
|
||||
void MarkUnusedAddresses(const CScript& script) override;
|
||||
|
||||
bool IsHDEnabled() const override;
|
||||
|
||||
//! Setup descriptors based on the given CExtkey
|
||||
bool SetupDescriptorGeneration(const CExtKey& master_key, OutputType addr_type, bool internal);
|
||||
|
||||
/** Provide a descriptor at setup time
|
||||
* Returns false if already setup or setup fails, true if setup is successful
|
||||
*/
|
||||
bool SetupDescriptor(std::unique_ptr<Descriptor>desc);
|
||||
|
||||
bool HavePrivateKeys() const override;
|
||||
|
||||
int64_t GetOldestKeyPoolTime() const override;
|
||||
unsigned int GetKeyPoolSize() const override;
|
||||
|
||||
int64_t GetTimeFirstKey() const override;
|
||||
|
||||
std::unique_ptr<CKeyMetadata> GetMetadata(const CTxDestination& dest) const override;
|
||||
|
||||
bool CanGetAddresses(bool internal = false) const override;
|
||||
|
||||
std::unique_ptr<SigningProvider> GetSolvingProvider(const CScript& script) const override;
|
||||
|
||||
bool CanProvide(const CScript& script, SignatureData& sigdata) override;
|
||||
|
||||
bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const override;
|
||||
SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const override;
|
||||
TransactionError FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override;
|
||||
|
||||
uint256 GetID() const override;
|
||||
|
||||
void SetCache(const DescriptorCache& cache);
|
||||
|
||||
bool AddKey(const CKeyID& key_id, const CKey& key);
|
||||
bool AddCryptedKey(const CKeyID& key_id, const CPubKey& pubkey, const std::vector<unsigned char>& crypted_key);
|
||||
|
||||
bool HasWalletDescriptor(const WalletDescriptor& desc) const;
|
||||
void UpdateWalletDescriptor(WalletDescriptor& descriptor);
|
||||
bool CanUpdateToWalletDescriptor(const WalletDescriptor& descriptor, std::string& error);
|
||||
void AddDescriptorKey(const CKey& key, const CPubKey &pubkey);
|
||||
void WriteDescriptor();
|
||||
|
||||
const WalletDescriptor GetWalletDescriptor() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
|
||||
const std::vector<CScript> GetScriptPubKeys() const;
|
||||
|
||||
bool GetDescriptorString(std::string& out) const;
|
||||
|
||||
void UpgradeDescriptorCache();
|
||||
};
|
||||
|
||||
#endif // DIGIBYTE_WALLET_SCRIPTPUBKEYMAN_H
|
||||
977
src/wallet/spend.cpp
Normal file
977
src/wallet/spend.cpp
Normal file
|
|
@ -0,0 +1,977 @@
|
|||
// Copyright (c) 2021 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <consensus/validation.h>
|
||||
#include <interfaces/chain.h>
|
||||
#include <policy/policy.h>
|
||||
#include <util/check.h>
|
||||
#include <util/fees.h>
|
||||
#include <util/moneystr.h>
|
||||
#include <util/rbf.h>
|
||||
#include <util/translation.h>
|
||||
#include <wallet/coincontrol.h>
|
||||
#include <wallet/fees.h>
|
||||
#include <wallet/receive.h>
|
||||
#include <wallet/spend.h>
|
||||
#include <wallet/transaction.h>
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
using interfaces::FoundBlock;
|
||||
|
||||
static constexpr size_t OUTPUT_GROUP_MAX_ENTRIES{100};
|
||||
|
||||
std::string COutput::ToString() const
|
||||
{
|
||||
return strprintf("COutput(%s, %d, %d) [%s]", tx->GetHash().ToString(), i, nDepth, FormatMoney(tx->tx->vout[i].nValue));
|
||||
}
|
||||
|
||||
int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* wallet, bool use_max_sig)
|
||||
{
|
||||
CMutableTransaction txn;
|
||||
txn.vin.push_back(CTxIn(COutPoint()));
|
||||
if (!wallet->DummySignInput(txn.vin[0], txout, use_max_sig)) {
|
||||
return -1;
|
||||
}
|
||||
return GetVirtualTransactionInputSize(txn.vin[0]);
|
||||
}
|
||||
|
||||
// txouts needs to be in the order of tx.vin
|
||||
TxSize CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, const std::vector<CTxOut>& txouts, bool use_max_sig)
|
||||
{
|
||||
CMutableTransaction txNew(tx);
|
||||
if (!wallet->DummySignTx(txNew, txouts, use_max_sig)) {
|
||||
return TxSize{-1, -1};
|
||||
}
|
||||
CTransaction ctx(txNew);
|
||||
int64_t vsize = GetVirtualTransactionSize(ctx);
|
||||
int64_t weight = GetTransactionWeight(ctx);
|
||||
return TxSize{vsize, weight};
|
||||
}
|
||||
|
||||
TxSize CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, bool use_max_sig)
|
||||
{
|
||||
std::vector<CTxOut> txouts;
|
||||
for (const CTxIn& input : tx.vin) {
|
||||
const auto mi = wallet->mapWallet.find(input.prevout.hash);
|
||||
// Can not estimate size without knowing the input details
|
||||
if (mi == wallet->mapWallet.end()) {
|
||||
return TxSize{-1, -1};
|
||||
}
|
||||
assert(input.prevout.n < mi->second.tx->vout.size());
|
||||
txouts.emplace_back(mi->second.tx->vout[input.prevout.n]);
|
||||
}
|
||||
return CalculateMaximumSignedTxSize(tx, wallet, txouts, use_max_sig);
|
||||
}
|
||||
|
||||
void CWallet::AvailableCoins(std::vector<COutput>& vCoins, const CCoinControl* coinControl, const CAmount& nMinimumAmount, const CAmount& nMaximumAmount, const CAmount& nMinimumSumAmount, const uint64_t nMaximumCount) const
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
|
||||
vCoins.clear();
|
||||
CAmount nTotal = 0;
|
||||
// Either the WALLET_FLAG_AVOID_REUSE flag is not set (in which case we always allow), or we default to avoiding, and only in the case where
|
||||
// a coin control object is provided, and has the avoid address reuse flag set to false, do we allow already used addresses
|
||||
bool allow_used_addresses = !IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE) || (coinControl && !coinControl->m_avoid_address_reuse);
|
||||
const int min_depth = {coinControl ? coinControl->m_min_depth : DEFAULT_MIN_DEPTH};
|
||||
const int max_depth = {coinControl ? coinControl->m_max_depth : DEFAULT_MAX_DEPTH};
|
||||
const bool only_safe = {coinControl ? !coinControl->m_include_unsafe_inputs : true};
|
||||
|
||||
std::set<uint256> trusted_parents;
|
||||
for (const auto& entry : mapWallet)
|
||||
{
|
||||
const uint256& wtxid = entry.first;
|
||||
const CWalletTx& wtx = entry.second;
|
||||
|
||||
if (!chain().checkFinalTx(*wtx.tx)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wtx.IsImmatureCoinBase())
|
||||
continue;
|
||||
|
||||
int nDepth = wtx.GetDepthInMainChain();
|
||||
if (nDepth < 0)
|
||||
continue;
|
||||
|
||||
// We should not consider coins which aren't at least in our mempool
|
||||
// It's possible for these to be conflicted via ancestors which we may never be able to detect
|
||||
if (nDepth == 0 && !wtx.InMempool())
|
||||
continue;
|
||||
|
||||
bool safeTx = IsTrusted(wtx, trusted_parents);
|
||||
|
||||
// We should not consider coins from transactions that are replacing
|
||||
// other transactions.
|
||||
//
|
||||
// Example: There is a transaction A which is replaced by bumpfee
|
||||
// transaction B. In this case, we want to prevent creation of
|
||||
// a transaction B' which spends an output of B.
|
||||
//
|
||||
// Reason: If transaction A were initially confirmed, transactions B
|
||||
// and B' would no longer be valid, so the user would have to create
|
||||
// a new transaction C to replace B'. However, in the case of a
|
||||
// one-block reorg, transactions B' and C might BOTH be accepted,
|
||||
// when the user only wanted one of them. Specifically, there could
|
||||
// be a 1-block reorg away from the chain where transactions A and C
|
||||
// were accepted to another chain where B, B', and C were all
|
||||
// accepted.
|
||||
if (nDepth == 0 && wtx.mapValue.count("replaces_txid")) {
|
||||
safeTx = false;
|
||||
}
|
||||
|
||||
// Similarly, we should not consider coins from transactions that
|
||||
// have been replaced. In the example above, we would want to prevent
|
||||
// creation of a transaction A' spending an output of A, because if
|
||||
// transaction B were initially confirmed, conflicting with A and
|
||||
// A', we wouldn't want to the user to create a transaction D
|
||||
// intending to replace A', but potentially resulting in a scenario
|
||||
// where A, A', and D could all be accepted (instead of just B and
|
||||
// D, or just A and A' like the user would want).
|
||||
if (nDepth == 0 && wtx.mapValue.count("replaced_by_txid")) {
|
||||
safeTx = false;
|
||||
}
|
||||
|
||||
if (only_safe && !safeTx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nDepth < min_depth || nDepth > max_depth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) {
|
||||
// Only consider selected coins if add_inputs is false
|
||||
if (coinControl && !coinControl->m_add_inputs && !coinControl->IsSelected(COutPoint(entry.first, i))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wtx.tx->vout[i].nValue < nMinimumAmount || wtx.tx->vout[i].nValue > nMaximumAmount)
|
||||
continue;
|
||||
|
||||
if (coinControl && coinControl->HasSelected() && !coinControl->fAllowOtherInputs && !coinControl->IsSelected(COutPoint(entry.first, i)))
|
||||
continue;
|
||||
|
||||
if (IsLockedCoin(entry.first, i))
|
||||
continue;
|
||||
|
||||
if (IsSpent(wtxid, i))
|
||||
continue;
|
||||
|
||||
isminetype mine = IsMine(wtx.tx->vout[i]);
|
||||
|
||||
if (mine == ISMINE_NO) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!allow_used_addresses && IsSpentKey(wtxid, i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::unique_ptr<SigningProvider> provider = GetSolvingProvider(wtx.tx->vout[i].scriptPubKey);
|
||||
|
||||
bool solvable = provider ? IsSolvable(*provider, wtx.tx->vout[i].scriptPubKey) : false;
|
||||
bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable));
|
||||
|
||||
vCoins.push_back(COutput(&wtx, i, nDepth, spendable, solvable, safeTx, (coinControl && coinControl->fAllowWatchOnly)));
|
||||
|
||||
// Checks the sum amount of all UTXO's.
|
||||
if (nMinimumSumAmount != MAX_MONEY) {
|
||||
nTotal += wtx.tx->vout[i].nValue;
|
||||
|
||||
if (nTotal >= nMinimumSumAmount) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Checks the maximum number of UTXO's.
|
||||
if (nMaximumCount > 0 && vCoins.size() >= nMaximumCount) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CAmount CWallet::GetAvailableBalance(const CCoinControl* coinControl) const
|
||||
{
|
||||
LOCK(cs_wallet);
|
||||
|
||||
CAmount balance = 0;
|
||||
std::vector<COutput> vCoins;
|
||||
AvailableCoins(vCoins, coinControl);
|
||||
for (const COutput& out : vCoins) {
|
||||
if (out.fSpendable) {
|
||||
balance += out.tx->tx->vout[out.i].nValue;
|
||||
}
|
||||
}
|
||||
return balance;
|
||||
}
|
||||
|
||||
const CTxOut& CWallet::FindNonChangeParentOutput(const CTransaction& tx, int output) const
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
const CTransaction* ptx = &tx;
|
||||
int n = output;
|
||||
while (IsChange(ptx->vout[n]) && ptx->vin.size() > 0) {
|
||||
const COutPoint& prevout = ptx->vin[0].prevout;
|
||||
auto it = mapWallet.find(prevout.hash);
|
||||
if (it == mapWallet.end() || it->second.tx->vout.size() <= prevout.n ||
|
||||
!IsMine(it->second.tx->vout[prevout.n])) {
|
||||
break;
|
||||
}
|
||||
ptx = it->second.tx.get();
|
||||
n = prevout.n;
|
||||
}
|
||||
return ptx->vout[n];
|
||||
}
|
||||
|
||||
std::map<CTxDestination, std::vector<COutput>> CWallet::ListCoins() const
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
|
||||
std::map<CTxDestination, std::vector<COutput>> result;
|
||||
std::vector<COutput> availableCoins;
|
||||
|
||||
AvailableCoins(availableCoins);
|
||||
|
||||
for (const COutput& coin : availableCoins) {
|
||||
CTxDestination address;
|
||||
if ((coin.fSpendable || (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && coin.fSolvable)) &&
|
||||
ExtractDestination(FindNonChangeParentOutput(*coin.tx->tx, coin.i).scriptPubKey, address)) {
|
||||
result[address].emplace_back(std::move(coin));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<COutPoint> lockedCoins;
|
||||
ListLockedCoins(lockedCoins);
|
||||
// Include watch-only for LegacyScriptPubKeyMan wallets without private keys
|
||||
const bool include_watch_only = GetLegacyScriptPubKeyMan() && IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
|
||||
const isminetype is_mine_filter = include_watch_only ? ISMINE_WATCH_ONLY : ISMINE_SPENDABLE;
|
||||
for (const COutPoint& output : lockedCoins) {
|
||||
auto it = mapWallet.find(output.hash);
|
||||
if (it != mapWallet.end()) {
|
||||
int depth = it->second.GetDepthInMainChain();
|
||||
if (depth >= 0 && output.n < it->second.tx->vout.size() &&
|
||||
IsMine(it->second.tx->vout[output.n]) == is_mine_filter
|
||||
) {
|
||||
CTxDestination address;
|
||||
if (ExtractDestination(FindNonChangeParentOutput(*it->second.tx, output.n).scriptPubKey, address)) {
|
||||
result[address].emplace_back(
|
||||
&it->second, output.n, depth, true /* spendable */, true /* solvable */, false /* safe */);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutput>& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only) const
|
||||
{
|
||||
std::vector<OutputGroup> groups_out;
|
||||
|
||||
if (!coin_sel_params.m_avoid_partial_spends) {
|
||||
// Allowing partial spends means no grouping. Each COutput gets its own OutputGroup.
|
||||
for (const COutput& output : outputs) {
|
||||
// Skip outputs we cannot spend
|
||||
if (!output.fSpendable) continue;
|
||||
|
||||
size_t ancestors, descendants;
|
||||
chain().getTransactionAncestry(output.tx->GetHash(), ancestors, descendants);
|
||||
CInputCoin input_coin = output.GetInputCoin();
|
||||
|
||||
// Make an OutputGroup containing just this output
|
||||
OutputGroup group{coin_sel_params};
|
||||
group.Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants, positive_only);
|
||||
|
||||
// Check the OutputGroup's eligibility. Only add the eligible ones.
|
||||
if (positive_only && group.GetSelectionAmount() <= 0) continue;
|
||||
if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter)) groups_out.push_back(group);
|
||||
}
|
||||
return groups_out;
|
||||
}
|
||||
|
||||
// We want to combine COutputs that have the same scriptPubKey into single OutputGroups
|
||||
// except when there are more than OUTPUT_GROUP_MAX_ENTRIES COutputs grouped in an OutputGroup.
|
||||
// To do this, we maintain a map where the key is the scriptPubKey and the value is a vector of OutputGroups.
|
||||
// For each COutput, we check if the scriptPubKey is in the map, and if it is, the COutput's CInputCoin is added
|
||||
// to the last OutputGroup in the vector for the scriptPubKey. When the last OutputGroup has
|
||||
// OUTPUT_GROUP_MAX_ENTRIES CInputCoins, a new OutputGroup is added to the end of the vector.
|
||||
std::map<CScript, std::vector<OutputGroup>> spk_to_groups_map;
|
||||
for (const auto& output : outputs) {
|
||||
// Skip outputs we cannot spend
|
||||
if (!output.fSpendable) continue;
|
||||
|
||||
size_t ancestors, descendants;
|
||||
chain().getTransactionAncestry(output.tx->GetHash(), ancestors, descendants);
|
||||
CInputCoin input_coin = output.GetInputCoin();
|
||||
CScript spk = input_coin.txout.scriptPubKey;
|
||||
|
||||
std::vector<OutputGroup>& groups = spk_to_groups_map[spk];
|
||||
|
||||
if (groups.size() == 0) {
|
||||
// No OutputGroups for this scriptPubKey yet, add one
|
||||
groups.emplace_back(coin_sel_params);
|
||||
}
|
||||
|
||||
// Get the last OutputGroup in the vector so that we can add the CInputCoin to it
|
||||
// A pointer is used here so that group can be reassigned later if it is full.
|
||||
OutputGroup* group = &groups.back();
|
||||
|
||||
// Check if this OutputGroup is full. We limit to OUTPUT_GROUP_MAX_ENTRIES when using -avoidpartialspends
|
||||
// to avoid surprising users with very high fees.
|
||||
if (group->m_outputs.size() >= OUTPUT_GROUP_MAX_ENTRIES) {
|
||||
// The last output group is full, add a new group to the vector and use that group for the insertion
|
||||
groups.emplace_back(coin_sel_params);
|
||||
group = &groups.back();
|
||||
}
|
||||
|
||||
// Add the input_coin to group
|
||||
group->Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants, positive_only);
|
||||
}
|
||||
|
||||
// Now we go through the entire map and pull out the OutputGroups
|
||||
for (const auto& spk_and_groups_pair: spk_to_groups_map) {
|
||||
const std::vector<OutputGroup>& groups_per_spk= spk_and_groups_pair.second;
|
||||
|
||||
// Go through the vector backwards. This allows for the first item we deal with being the partial group.
|
||||
for (auto group_it = groups_per_spk.rbegin(); group_it != groups_per_spk.rend(); group_it++) {
|
||||
const OutputGroup& group = *group_it;
|
||||
|
||||
// Don't include partial groups if there are full groups too and we don't want partial groups
|
||||
if (group_it == groups_per_spk.rbegin() && groups_per_spk.size() > 1 && !filter.m_include_partial_groups) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check the OutputGroup's eligibility. Only add the eligible ones.
|
||||
if (positive_only && group.GetSelectionAmount() <= 0) continue;
|
||||
if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter)) groups_out.push_back(group);
|
||||
}
|
||||
}
|
||||
|
||||
return groups_out;
|
||||
}
|
||||
|
||||
bool CWallet::AttemptSelection(const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector<COutput> coins,
|
||||
std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet, const CoinSelectionParams& coin_selection_params) const
|
||||
{
|
||||
setCoinsRet.clear();
|
||||
nValueRet = 0;
|
||||
|
||||
// Note that unlike KnapsackSolver, we do not include the fee for creating a change output as BnB will not create a change output.
|
||||
std::vector<OutputGroup> positive_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, true /* positive_only */);
|
||||
if (SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change, setCoinsRet, nValueRet)) {
|
||||
return true;
|
||||
}
|
||||
// The knapsack solver has some legacy behavior where it will spend dust outputs. We retain this behavior, so don't filter for positive only here.
|
||||
std::vector<OutputGroup> all_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, false /* positive_only */);
|
||||
// While nTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output.
|
||||
// So we need to include that for KnapsackSolver as well, as we are expecting to create a change output.
|
||||
return KnapsackSolver(nTargetValue + coin_selection_params.m_change_fee, all_groups, setCoinsRet, nValueRet);
|
||||
}
|
||||
|
||||
bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAmount& nTargetValue, std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet, const CCoinControl& coin_control, CoinSelectionParams& coin_selection_params) const
|
||||
{
|
||||
std::vector<COutput> vCoins(vAvailableCoins);
|
||||
CAmount value_to_select = nTargetValue;
|
||||
|
||||
// coin control -> return all selected outputs (we want all selected to go into the transaction for sure)
|
||||
if (coin_control.HasSelected() && !coin_control.fAllowOtherInputs)
|
||||
{
|
||||
for (const COutput& out : vCoins)
|
||||
{
|
||||
if (!out.fSpendable)
|
||||
continue;
|
||||
nValueRet += out.tx->tx->vout[out.i].nValue;
|
||||
setCoinsRet.insert(out.GetInputCoin());
|
||||
}
|
||||
return (nValueRet >= nTargetValue);
|
||||
}
|
||||
|
||||
// calculate value from preset inputs and store them
|
||||
std::set<CInputCoin> setPresetCoins;
|
||||
CAmount nValueFromPresetInputs = 0;
|
||||
|
||||
std::vector<COutPoint> vPresetInputs;
|
||||
coin_control.ListSelected(vPresetInputs);
|
||||
for (const COutPoint& outpoint : vPresetInputs)
|
||||
{
|
||||
std::map<uint256, CWalletTx>::const_iterator it = mapWallet.find(outpoint.hash);
|
||||
if (it != mapWallet.end())
|
||||
{
|
||||
const CWalletTx& wtx = it->second;
|
||||
// Clearly invalid input, fail
|
||||
if (wtx.tx->vout.size() <= outpoint.n) {
|
||||
return false;
|
||||
}
|
||||
// Just to calculate the marginal byte size
|
||||
CInputCoin coin(wtx.tx, outpoint.n, wtx.GetSpendSize(outpoint.n, false));
|
||||
nValueFromPresetInputs += coin.txout.nValue;
|
||||
if (coin.m_input_bytes <= 0) {
|
||||
return false; // Not solvable, can't estimate size for fee
|
||||
}
|
||||
coin.effective_value = coin.txout.nValue - coin_selection_params.m_effective_feerate.GetFee(coin.m_input_bytes);
|
||||
if (coin_selection_params.m_subtract_fee_outputs) {
|
||||
value_to_select -= coin.txout.nValue;
|
||||
} else {
|
||||
value_to_select -= coin.effective_value;
|
||||
}
|
||||
setPresetCoins.insert(coin);
|
||||
} else {
|
||||
return false; // TODO: Allow non-wallet inputs
|
||||
}
|
||||
}
|
||||
|
||||
// remove preset inputs from vCoins so that Coin Selection doesn't pick them.
|
||||
for (std::vector<COutput>::iterator it = vCoins.begin(); it != vCoins.end() && coin_control.HasSelected();)
|
||||
{
|
||||
if (setPresetCoins.count(it->GetInputCoin()))
|
||||
it = vCoins.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
|
||||
unsigned int limit_ancestor_count = 0;
|
||||
unsigned int limit_descendant_count = 0;
|
||||
chain().getPackageLimits(limit_ancestor_count, limit_descendant_count);
|
||||
const size_t max_ancestors = (size_t)std::max<int64_t>(1, limit_ancestor_count);
|
||||
const size_t max_descendants = (size_t)std::max<int64_t>(1, limit_descendant_count);
|
||||
const bool fRejectLongChains = gArgs.GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS);
|
||||
|
||||
// form groups from remaining coins; note that preset coins will not
|
||||
// automatically have their associated (same address) coins included
|
||||
if (coin_control.m_avoid_partial_spends && vCoins.size() > OUTPUT_GROUP_MAX_ENTRIES) {
|
||||
// Cases where we have 101+ outputs all pointing to the same destination may result in
|
||||
// privacy leaks as they will potentially be deterministically sorted. We solve that by
|
||||
// explicitly shuffling the outputs before processing
|
||||
Shuffle(vCoins.begin(), vCoins.end(), FastRandomContext());
|
||||
}
|
||||
|
||||
// Coin Selection attempts to select inputs from a pool of eligible UTXOs to fund the
|
||||
// transaction at a target feerate. If an attempt fails, more attempts may be made using a more
|
||||
// permissive CoinEligibilityFilter.
|
||||
const bool res = [&] {
|
||||
// Pre-selected inputs already cover the target amount.
|
||||
if (value_to_select <= 0) return true;
|
||||
|
||||
// If possible, fund the transaction with confirmed UTXOs only. Prefer at least six
|
||||
// confirmations on outputs received from other wallets and only spend confirmed change.
|
||||
if (AttemptSelection(value_to_select, CoinEligibilityFilter(1, 6, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params)) return true;
|
||||
if (AttemptSelection(value_to_select, CoinEligibilityFilter(1, 1, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params)) return true;
|
||||
|
||||
// Fall back to using zero confirmation change (but with as few ancestors in the mempool as
|
||||
// possible) if we cannot fund the transaction otherwise.
|
||||
if (m_spend_zero_conf_change) {
|
||||
if (AttemptSelection(value_to_select, CoinEligibilityFilter(0, 1, 2), vCoins, setCoinsRet, nValueRet, coin_selection_params)) return true;
|
||||
if (AttemptSelection(value_to_select, CoinEligibilityFilter(0, 1, std::min((size_t)4, max_ancestors/3), std::min((size_t)4, max_descendants/3)),
|
||||
vCoins, setCoinsRet, nValueRet, coin_selection_params)) {
|
||||
return true;
|
||||
}
|
||||
if (AttemptSelection(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors/2, max_descendants/2),
|
||||
vCoins, setCoinsRet, nValueRet, coin_selection_params)) {
|
||||
return true;
|
||||
}
|
||||
// If partial groups are allowed, relax the requirement of spending OutputGroups (groups
|
||||
// of UTXOs sent to the same address, which are obviously controlled by a single wallet)
|
||||
// in their entirety.
|
||||
if (AttemptSelection(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors-1, max_descendants-1, true /* include_partial_groups */),
|
||||
vCoins, setCoinsRet, nValueRet, coin_selection_params)) {
|
||||
return true;
|
||||
}
|
||||
// Try with unsafe inputs if they are allowed. This may spend unconfirmed outputs
|
||||
// received from other wallets.
|
||||
if (coin_control.m_include_unsafe_inputs
|
||||
&& AttemptSelection(value_to_select,
|
||||
CoinEligibilityFilter(0 /* conf_mine */, 0 /* conf_theirs */, max_ancestors-1, max_descendants-1, true /* include_partial_groups */),
|
||||
vCoins, setCoinsRet, nValueRet, coin_selection_params)) {
|
||||
return true;
|
||||
}
|
||||
// Try with unlimited ancestors/descendants. The transaction will still need to meet
|
||||
// mempool ancestor/descendant policy to be accepted to mempool and broadcasted, but
|
||||
// OutputGroups use heuristics that may overestimate ancestor/descendant counts.
|
||||
if (!fRejectLongChains && AttemptSelection(value_to_select,
|
||||
CoinEligibilityFilter(0, 1, std::numeric_limits<uint64_t>::max(), std::numeric_limits<uint64_t>::max(), true /* include_partial_groups */),
|
||||
vCoins, setCoinsRet, nValueRet, coin_selection_params)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Coin Selection failed.
|
||||
return false;
|
||||
}();
|
||||
|
||||
// AttemptSelection clears setCoinsRet, so add the preset inputs from coin_control to the coinset
|
||||
util::insert(setCoinsRet, setPresetCoins);
|
||||
|
||||
// add preset inputs to the total value selected
|
||||
nValueRet += nValueFromPresetInputs;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static bool IsCurrentForAntiFeeSniping(interfaces::Chain& chain, const uint256& block_hash)
|
||||
{
|
||||
if (chain.isInitialBlockDownload()) {
|
||||
return false;
|
||||
}
|
||||
constexpr int64_t MAX_ANTI_FEE_SNIPING_TIP_AGE = 8 * 60 * 60; // in seconds
|
||||
int64_t block_time;
|
||||
CHECK_NONFATAL(chain.findBlock(block_hash, FoundBlock().time(block_time)));
|
||||
if (block_time < (GetTime() - MAX_ANTI_FEE_SNIPING_TIP_AGE)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a height-based locktime for new transactions (uses the height of the
|
||||
* current chain tip unless we are not synced with the current chain
|
||||
*/
|
||||
static uint32_t GetLocktimeForNewTransaction(interfaces::Chain& chain, const uint256& block_hash, int block_height)
|
||||
{
|
||||
uint32_t locktime;
|
||||
// Discourage fee sniping.
|
||||
//
|
||||
// For a large miner the value of the transactions in the best block and
|
||||
// the mempool can exceed the cost of deliberately attempting to mine two
|
||||
// blocks to orphan the current best block. By setting nLockTime such that
|
||||
// only the next block can include the transaction, we discourage this
|
||||
// practice as the height restricted and limited blocksize gives miners
|
||||
// considering fee sniping fewer options for pulling off this attack.
|
||||
//
|
||||
// A simple way to think about this is from the wallet's point of view we
|
||||
// always want the blockchain to move forward. By setting nLockTime this
|
||||
// way we're basically making the statement that we only want this
|
||||
// transaction to appear in the next block; we don't want to potentially
|
||||
// encourage reorgs by allowing transactions to appear at lower heights
|
||||
// than the next block in forks of the best chain.
|
||||
//
|
||||
// Of course, the subsidy is high enough, and transaction volume low
|
||||
// enough, that fee sniping isn't a problem yet, but by implementing a fix
|
||||
// now we ensure code won't be written that makes assumptions about
|
||||
// nLockTime that preclude a fix later.
|
||||
if (IsCurrentForAntiFeeSniping(chain, block_hash)) {
|
||||
locktime = block_height;
|
||||
|
||||
// Secondly occasionally randomly pick a nLockTime even further back, so
|
||||
// that transactions that are delayed after signing for whatever reason,
|
||||
// e.g. high-latency mix networks and some CoinJoin implementations, have
|
||||
// better privacy.
|
||||
if (GetRandInt(10) == 0)
|
||||
locktime = std::max(0, (int)locktime - GetRandInt(100));
|
||||
} else {
|
||||
// If our chain is lagging behind, we can't discourage fee sniping nor help
|
||||
// the privacy of high-latency transactions. To avoid leaking a potentially
|
||||
// unique "nLockTime fingerprint", set nLockTime to a constant.
|
||||
locktime = 0;
|
||||
}
|
||||
assert(locktime < LOCKTIME_THRESHOLD);
|
||||
return locktime;
|
||||
}
|
||||
|
||||
bool CWallet::CreateTransactionInternal(
|
||||
const std::vector<CRecipient>& vecSend,
|
||||
CTransactionRef& tx,
|
||||
CAmount& nFeeRet,
|
||||
int& nChangePosInOut,
|
||||
bilingual_str& error,
|
||||
const CCoinControl& coin_control,
|
||||
FeeCalculation& fee_calc_out,
|
||||
bool sign)
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
|
||||
CMutableTransaction txNew; // The resulting transaction that we make
|
||||
txNew.nLockTime = GetLocktimeForNewTransaction(chain(), GetLastBlockHash(), GetLastBlockHeight());
|
||||
|
||||
CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy
|
||||
coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends;
|
||||
|
||||
CAmount recipients_sum = 0;
|
||||
const OutputType change_type = TransactionChangeType(coin_control.m_change_type ? *coin_control.m_change_type : m_default_change_type, vecSend);
|
||||
ReserveDestination reservedest(this, change_type);
|
||||
unsigned int outputs_to_subtract_fee_from = 0; // The number of outputs which we are subtracting the fee from
|
||||
for (const auto& recipient : vecSend) {
|
||||
recipients_sum += recipient.nAmount;
|
||||
|
||||
if (recipient.fSubtractFeeFromAmount) {
|
||||
outputs_to_subtract_fee_from++;
|
||||
coin_selection_params.m_subtract_fee_outputs = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Create change script that will be used if we need change
|
||||
// TODO: pass in scriptChange instead of reservedest so
|
||||
// change transaction isn't always pay-to-digibyte-address
|
||||
CScript scriptChange;
|
||||
|
||||
// coin control: send change to custom address
|
||||
if (!std::get_if<CNoDestination>(&coin_control.destChange)) {
|
||||
scriptChange = GetScriptForDestination(coin_control.destChange);
|
||||
} else { // no coin control: send change to newly generated address
|
||||
// Note: We use a new key here to keep it from being obvious which side is the change.
|
||||
// The drawback is that by not reusing a previous key, the change may be lost if a
|
||||
// backup is restored, if the backup doesn't have the new private key for the change.
|
||||
// If we reused the old key, it would be possible to add code to look for and
|
||||
// rediscover unknown transactions that were written with keys of ours to recover
|
||||
// post-backup change.
|
||||
|
||||
// Reserve a new key pair from key pool. If it fails, provide a dummy
|
||||
// destination in case we don't need change.
|
||||
CTxDestination dest;
|
||||
std::string dest_err;
|
||||
if (!reservedest.GetReservedDestination(dest, true, dest_err)) {
|
||||
error = strprintf(_("Transaction needs a change address, but we can't generate it. %s"), dest_err);
|
||||
}
|
||||
scriptChange = GetScriptForDestination(dest);
|
||||
// A valid destination implies a change script (and
|
||||
// vice-versa). An empty change script will abort later, if the
|
||||
// change keypool ran out, but change is required.
|
||||
CHECK_NONFATAL(IsValidDestination(dest) != scriptChange.empty());
|
||||
}
|
||||
CTxOut change_prototype_txout(0, scriptChange);
|
||||
coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout);
|
||||
|
||||
// Get size of spending the change output
|
||||
int change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, this);
|
||||
// If the wallet doesn't know how to sign change output, assume p2sh-p2wpkh
|
||||
// as lower-bound to allow BnB to do it's thing
|
||||
if (change_spend_size == -1) {
|
||||
coin_selection_params.change_spend_size = DUMMY_NESTED_P2WPKH_INPUT_SIZE;
|
||||
} else {
|
||||
coin_selection_params.change_spend_size = (size_t)change_spend_size;
|
||||
}
|
||||
|
||||
// Set discard feerate
|
||||
coin_selection_params.m_discard_feerate = GetDiscardRate(*this);
|
||||
|
||||
// Get the fee rate to use effective values in coin selection
|
||||
FeeCalculation feeCalc;
|
||||
coin_selection_params.m_effective_feerate = GetMinimumFeeRate(*this, coin_control, &feeCalc);
|
||||
// Do not, ever, assume that it's fine to change the fee rate if the user has explicitly
|
||||
// provided one
|
||||
if (coin_control.m_feerate && coin_selection_params.m_effective_feerate > *coin_control.m_feerate) {
|
||||
error = strprintf(_("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), coin_control.m_feerate->ToString(FeeEstimateMode::SAT_VB), coin_selection_params.m_effective_feerate.ToString(FeeEstimateMode::SAT_VB));
|
||||
return false;
|
||||
}
|
||||
if (feeCalc.reason == FeeReason::FALLBACK && !m_allow_fallback_fee) {
|
||||
// eventually allow a fallback fee
|
||||
error = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get long term estimate
|
||||
CCoinControl cc_temp;
|
||||
cc_temp.m_confirm_target = chain().estimateMaxBlocks();
|
||||
coin_selection_params.m_long_term_feerate = GetMinimumFeeRate(*this, cc_temp, nullptr);
|
||||
|
||||
// Calculate the cost of change
|
||||
// Cost of change is the cost of creating the change output + cost of spending the change output in the future.
|
||||
// For creating the change output now, we use the effective feerate.
|
||||
// For spending the change output in the future, we use the discard feerate for now.
|
||||
// So cost of change = (change output size * effective feerate) + (size of spending change output * discard feerate)
|
||||
coin_selection_params.m_change_fee = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.change_output_size);
|
||||
coin_selection_params.m_cost_of_change = coin_selection_params.m_discard_feerate.GetFee(coin_selection_params.change_spend_size) + coin_selection_params.m_change_fee;
|
||||
|
||||
// vouts to the payees
|
||||
if (!coin_selection_params.m_subtract_fee_outputs) {
|
||||
coin_selection_params.tx_noinputs_size = 11; // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count, 1 output count, 1 witness overhead (dummy, flag, stack size)
|
||||
}
|
||||
for (const auto& recipient : vecSend)
|
||||
{
|
||||
CTxOut txout(recipient.nAmount, recipient.scriptPubKey);
|
||||
|
||||
// Include the fee cost for outputs.
|
||||
if (!coin_selection_params.m_subtract_fee_outputs) {
|
||||
coin_selection_params.tx_noinputs_size += ::GetSerializeSize(txout, PROTOCOL_VERSION);
|
||||
}
|
||||
|
||||
if (IsDust(txout, chain().relayDustFee()))
|
||||
{
|
||||
error = _("Transaction amount too small");
|
||||
return false;
|
||||
}
|
||||
txNew.vout.push_back(txout);
|
||||
}
|
||||
|
||||
// Include the fees for things that aren't inputs, excluding the change output
|
||||
const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size);
|
||||
CAmount selection_target = recipients_sum + not_input_fees;
|
||||
|
||||
// Get available coins
|
||||
std::vector<COutput> vAvailableCoins;
|
||||
AvailableCoins(vAvailableCoins, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0);
|
||||
|
||||
// Choose coins to use
|
||||
CAmount inputs_sum = 0;
|
||||
std::set<CInputCoin> setCoins;
|
||||
if (!SelectCoins(vAvailableCoins, /* nTargetValue */ selection_target, setCoins, inputs_sum, coin_control, coin_selection_params))
|
||||
{
|
||||
error = _("Insufficient funds");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Always make a change output
|
||||
// We will reduce the fee from this change output later, and remove the output if it is too small.
|
||||
const CAmount change_and_fee = inputs_sum - recipients_sum;
|
||||
assert(change_and_fee >= 0);
|
||||
CTxOut newTxOut(change_and_fee, scriptChange);
|
||||
|
||||
if (nChangePosInOut == -1)
|
||||
{
|
||||
// Insert change txn at random position:
|
||||
nChangePosInOut = GetRandInt(txNew.vout.size()+1);
|
||||
}
|
||||
else if ((unsigned int)nChangePosInOut > txNew.vout.size())
|
||||
{
|
||||
error = _("Change index out of range");
|
||||
return false;
|
||||
}
|
||||
|
||||
assert(nChangePosInOut != -1);
|
||||
auto change_position = txNew.vout.insert(txNew.vout.begin() + nChangePosInOut, newTxOut);
|
||||
|
||||
// Shuffle selected coins and fill in final vin
|
||||
std::vector<CInputCoin> selected_coins(setCoins.begin(), setCoins.end());
|
||||
Shuffle(selected_coins.begin(), selected_coins.end(), FastRandomContext());
|
||||
|
||||
// Note how the sequence number is set to non-maxint so that
|
||||
// the nLockTime set above actually works.
|
||||
//
|
||||
// BIP125 defines opt-in RBF as any nSequence < maxint-1, so
|
||||
// we use the highest possible value in that range (maxint-2)
|
||||
// to avoid conflicting with other possible uses of nSequence,
|
||||
// and in the spirit of "smallest possible change from prior
|
||||
// behavior."
|
||||
const uint32_t nSequence = coin_control.m_signal_bip125_rbf.value_or(m_signal_rbf) ? MAX_BIP125_RBF_SEQUENCE : (CTxIn::SEQUENCE_FINAL - 1);
|
||||
for (const auto& coin : selected_coins) {
|
||||
txNew.vin.push_back(CTxIn(coin.outpoint, CScript(), nSequence));
|
||||
}
|
||||
|
||||
// Calculate the transaction fee
|
||||
TxSize tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly);
|
||||
int nBytes = tx_sizes.vsize;
|
||||
if (nBytes < 0) {
|
||||
error = _("Signing transaction failed");
|
||||
return false;
|
||||
}
|
||||
nFeeRet = coin_selection_params.m_effective_feerate.GetFee(nBytes);
|
||||
|
||||
// Subtract fee from the change output if not subtracting it from recipient outputs
|
||||
CAmount fee_needed = nFeeRet;
|
||||
if (!coin_selection_params.m_subtract_fee_outputs) {
|
||||
change_position->nValue -= fee_needed;
|
||||
}
|
||||
|
||||
// We want to drop the change to fees if:
|
||||
// 1. The change output would be dust
|
||||
// 2. The change is within the (almost) exact match window, i.e. it is less than or equal to the cost of the change output (cost_of_change)
|
||||
CAmount change_amount = change_position->nValue;
|
||||
if (IsDust(*change_position, coin_selection_params.m_discard_feerate) || change_amount <= coin_selection_params.m_cost_of_change)
|
||||
{
|
||||
nChangePosInOut = -1;
|
||||
change_amount = 0;
|
||||
txNew.vout.erase(change_position);
|
||||
|
||||
// Because we have dropped this change, the tx size and required fee will be different, so let's recalculate those
|
||||
tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly);
|
||||
nBytes = tx_sizes.vsize;
|
||||
fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes);
|
||||
}
|
||||
|
||||
// The only time that fee_needed should be less than the amount available for fees (in change_and_fee - change_amount) is when
|
||||
// we are subtracting the fee from the outputs. If this occurs at any other time, it is a bug.
|
||||
assert(coin_selection_params.m_subtract_fee_outputs || fee_needed <= change_and_fee - change_amount);
|
||||
|
||||
// Update nFeeRet in case fee_needed changed due to dropping the change output
|
||||
if (fee_needed <= change_and_fee - change_amount) {
|
||||
nFeeRet = change_and_fee - change_amount;
|
||||
}
|
||||
|
||||
// Reduce output values for subtractFeeFromAmount
|
||||
if (coin_selection_params.m_subtract_fee_outputs) {
|
||||
CAmount to_reduce = fee_needed + change_amount - change_and_fee;
|
||||
int i = 0;
|
||||
bool fFirst = true;
|
||||
for (const auto& recipient : vecSend)
|
||||
{
|
||||
if (i == nChangePosInOut) {
|
||||
++i;
|
||||
}
|
||||
CTxOut& txout = txNew.vout[i];
|
||||
|
||||
if (recipient.fSubtractFeeFromAmount)
|
||||
{
|
||||
txout.nValue -= to_reduce / outputs_to_subtract_fee_from; // Subtract fee equally from each selected recipient
|
||||
|
||||
if (fFirst) // first receiver pays the remainder not divisible by output count
|
||||
{
|
||||
fFirst = false;
|
||||
txout.nValue -= to_reduce % outputs_to_subtract_fee_from;
|
||||
}
|
||||
|
||||
// Error if this output is reduced to be below dust
|
||||
if (IsDust(txout, chain().relayDustFee())) {
|
||||
if (txout.nValue < 0) {
|
||||
error = _("The transaction amount is too small to pay the fee");
|
||||
} else {
|
||||
error = _("The transaction amount is too small to send after the fee has been deducted");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
++i;
|
||||
}
|
||||
nFeeRet = fee_needed;
|
||||
}
|
||||
|
||||
// Give up if change keypool ran out and change is required
|
||||
if (scriptChange.empty() && nChangePosInOut != -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sign && !SignTransaction(txNew)) {
|
||||
error = _("Signing transaction failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return the constructed transaction data.
|
||||
tx = MakeTransactionRef(std::move(txNew));
|
||||
|
||||
// Limit size
|
||||
if ((sign && GetTransactionWeight(*tx) > MAX_STANDARD_TX_WEIGHT) ||
|
||||
(!sign && tx_sizes.weight > MAX_STANDARD_TX_WEIGHT))
|
||||
{
|
||||
error = _("Transaction too large");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nFeeRet > m_default_max_tx_fee) {
|
||||
error = TransactionErrorString(TransactionError::MAX_FEE_EXCEEDED);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gArgs.GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS)) {
|
||||
// Lastly, ensure this tx will pass the mempool's chain limits
|
||||
if (!chain().checkChainLimits(tx)) {
|
||||
error = _("Transaction has too long of a mempool chain");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Before we return success, we assume any change key will be used to prevent
|
||||
// accidental re-use.
|
||||
reservedest.KeepDestination();
|
||||
fee_calc_out = feeCalc;
|
||||
|
||||
WalletLogPrintf("Fee Calculation: Fee:%d Bytes:%u Tgt:%d (requested %d) Reason:\"%s\" Decay %.5f: Estimation: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out) Fail: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out)\n",
|
||||
nFeeRet, nBytes, feeCalc.returnedTarget, feeCalc.desiredTarget, StringForFeeReason(feeCalc.reason), feeCalc.est.decay,
|
||||
feeCalc.est.pass.start, feeCalc.est.pass.end,
|
||||
(feeCalc.est.pass.totalConfirmed + feeCalc.est.pass.inMempool + feeCalc.est.pass.leftMempool) > 0.0 ? 100 * feeCalc.est.pass.withinTarget / (feeCalc.est.pass.totalConfirmed + feeCalc.est.pass.inMempool + feeCalc.est.pass.leftMempool) : 0.0,
|
||||
feeCalc.est.pass.withinTarget, feeCalc.est.pass.totalConfirmed, feeCalc.est.pass.inMempool, feeCalc.est.pass.leftMempool,
|
||||
feeCalc.est.fail.start, feeCalc.est.fail.end,
|
||||
(feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) > 0.0 ? 100 * feeCalc.est.fail.withinTarget / (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) : 0.0,
|
||||
feeCalc.est.fail.withinTarget, feeCalc.est.fail.totalConfirmed, feeCalc.est.fail.inMempool, feeCalc.est.fail.leftMempool);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CWallet::CreateTransaction(
|
||||
const std::vector<CRecipient>& vecSend,
|
||||
CTransactionRef& tx,
|
||||
CAmount& nFeeRet,
|
||||
int& nChangePosInOut,
|
||||
bilingual_str& error,
|
||||
const CCoinControl& coin_control,
|
||||
FeeCalculation& fee_calc_out,
|
||||
bool sign)
|
||||
{
|
||||
if (vecSend.empty()) {
|
||||
error = _("Transaction must have at least one recipient");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (std::any_of(vecSend.cbegin(), vecSend.cend(), [](const auto& recipient){ return recipient.nAmount < 0; })) {
|
||||
error = _("Transaction amounts must not be negative");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOCK(cs_wallet);
|
||||
|
||||
int nChangePosIn = nChangePosInOut;
|
||||
Assert(!tx); // tx is an out-param. TODO change the return type from bool to tx (or nullptr)
|
||||
bool res = CreateTransactionInternal(vecSend, tx, nFeeRet, nChangePosInOut, error, coin_control, fee_calc_out, sign);
|
||||
// try with avoidpartialspends unless it's enabled already
|
||||
if (res && nFeeRet > 0 /* 0 means non-functional fee rate estimation */ && m_max_aps_fee > -1 && !coin_control.m_avoid_partial_spends) {
|
||||
CCoinControl tmp_cc = coin_control;
|
||||
tmp_cc.m_avoid_partial_spends = true;
|
||||
CAmount nFeeRet2;
|
||||
CTransactionRef tx2;
|
||||
int nChangePosInOut2 = nChangePosIn;
|
||||
bilingual_str error2; // fired and forgotten; if an error occurs, we discard the results
|
||||
if (CreateTransactionInternal(vecSend, tx2, nFeeRet2, nChangePosInOut2, error2, tmp_cc, fee_calc_out, sign)) {
|
||||
// if fee of this alternative one is within the range of the max fee, we use this one
|
||||
const bool use_aps = nFeeRet2 <= nFeeRet + m_max_aps_fee;
|
||||
std::cout << "nFeeRet2 = " << nFeeRet2 << std::endl;
|
||||
std::cout << "m_max_aps_fee = " << m_max_aps_fee << std::endl;
|
||||
std::cout << "nFeeRet = " << nFeeRet << std::endl;
|
||||
WalletLogPrintf("Fee non-grouped = %lld, grouped = %lld, using %s\n", nFeeRet, nFeeRet2, use_aps ? "grouped" : "non-grouped");
|
||||
if (use_aps) {
|
||||
tx = tx2;
|
||||
nFeeRet = nFeeRet2;
|
||||
nChangePosInOut = nChangePosInOut2;
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, int& nChangePosInOut, bilingual_str& error, bool lockUnspents, const std::set<int>& setSubtractFeeFromOutputs, CCoinControl coinControl)
|
||||
{
|
||||
std::vector<CRecipient> vecSend;
|
||||
|
||||
// Turn the txout set into a CRecipient vector.
|
||||
for (size_t idx = 0; idx < tx.vout.size(); idx++) {
|
||||
const CTxOut& txOut = tx.vout[idx];
|
||||
CRecipient recipient = {txOut.scriptPubKey, txOut.nValue, setSubtractFeeFromOutputs.count(idx) == 1};
|
||||
vecSend.push_back(recipient);
|
||||
}
|
||||
|
||||
coinControl.fAllowOtherInputs = true;
|
||||
|
||||
for (const CTxIn& txin : tx.vin) {
|
||||
coinControl.Select(txin.prevout);
|
||||
}
|
||||
|
||||
// Acquire the locks to prevent races to the new locked unspents between the
|
||||
// CreateTransaction call and LockCoin calls (when lockUnspents is true).
|
||||
LOCK(cs_wallet);
|
||||
|
||||
CTransactionRef tx_new;
|
||||
FeeCalculation fee_calc_out;
|
||||
if (!CreateTransaction(vecSend, tx_new, nFeeRet, nChangePosInOut, error, coinControl, fee_calc_out, false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nChangePosInOut != -1) {
|
||||
tx.vout.insert(tx.vout.begin() + nChangePosInOut, tx_new->vout[nChangePosInOut]);
|
||||
}
|
||||
|
||||
// Copy output sizes from new transaction; they may have had the fee
|
||||
// subtracted from them.
|
||||
for (unsigned int idx = 0; idx < tx.vout.size(); idx++) {
|
||||
tx.vout[idx].nValue = tx_new->vout[idx].nValue;
|
||||
}
|
||||
|
||||
// Add new txins while keeping original txin scriptSig/order.
|
||||
for (const CTxIn& txin : tx_new->vin) {
|
||||
if (!coinControl.IsSelected(txin.prevout)) {
|
||||
tx.vin.push_back(txin);
|
||||
|
||||
}
|
||||
if (lockUnspents) {
|
||||
LockCoin(txin.prevout);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
64
src/wallet/spend.h
Normal file
64
src/wallet/spend.h
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (c) 2021 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_SPEND_H
|
||||
#define DIGIBYTE_WALLET_SPEND_H
|
||||
|
||||
#include <wallet/coinselection.h>
|
||||
#include <wallet/transaction.h>
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
class COutput
|
||||
{
|
||||
public:
|
||||
const CWalletTx *tx;
|
||||
|
||||
/** Index in tx->vout. */
|
||||
int i;
|
||||
|
||||
/**
|
||||
* Depth in block chain.
|
||||
* If > 0: the tx is on chain and has this many confirmations.
|
||||
* If = 0: the tx is waiting confirmation.
|
||||
* If < 0: a conflicting tx is on chain and has this many confirmations. */
|
||||
int nDepth;
|
||||
|
||||
/** Pre-computed estimated size of this output as a fully-signed input in a transaction. Can be -1 if it could not be calculated */
|
||||
int nInputBytes;
|
||||
|
||||
/** Whether we have the private keys to spend this output */
|
||||
bool fSpendable;
|
||||
|
||||
/** Whether we know how to spend this output, ignoring the lack of keys */
|
||||
bool fSolvable;
|
||||
|
||||
/** Whether to use the maximum sized, 72 byte signature when calculating the size of the input spend. This should only be set when watch-only outputs are allowed */
|
||||
bool use_max_sig;
|
||||
|
||||
/**
|
||||
* Whether this output is considered safe to spend. Unconfirmed transactions
|
||||
* from outside keys and unconfirmed replacement transactions are considered
|
||||
* unsafe and will not be used to fund new spending transactions.
|
||||
*/
|
||||
bool fSafe;
|
||||
|
||||
COutput(const CWalletTx *txIn, int iIn, int nDepthIn, bool fSpendableIn, bool fSolvableIn, bool fSafeIn, bool use_max_sig_in = false)
|
||||
{
|
||||
tx = txIn; i = iIn; nDepth = nDepthIn; fSpendable = fSpendableIn; fSolvable = fSolvableIn; fSafe = fSafeIn; nInputBytes = -1; use_max_sig = use_max_sig_in;
|
||||
// If known and signable by the given wallet, compute nInputBytes
|
||||
// Failure will keep this value -1
|
||||
if (fSpendable && tx) {
|
||||
nInputBytes = tx->GetSpendSize(i, use_max_sig);
|
||||
}
|
||||
}
|
||||
|
||||
std::string ToString() const;
|
||||
|
||||
inline CInputCoin GetInputCoin() const
|
||||
{
|
||||
return CInputCoin(tx->tx, i, nInputBytes);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // DIGIBYTE_WALLET_SPEND_H
|
||||
576
src/wallet/sqlite.cpp
Normal file
576
src/wallet/sqlite.cpp
Normal file
|
|
@ -0,0 +1,576 @@
|
|||
// Copyright (c) 2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <wallet/sqlite.h>
|
||||
|
||||
#include <chainparams.h>
|
||||
#include <crypto/common.h>
|
||||
#include <logging.h>
|
||||
#include <sync.h>
|
||||
#include <util/strencodings.h>
|
||||
#include <util/system.h>
|
||||
#include <util/translation.h>
|
||||
#include <wallet/db.h>
|
||||
|
||||
#include <sqlite3.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
static constexpr int32_t WALLET_SCHEMA_VERSION = 0;
|
||||
|
||||
static Mutex g_sqlite_mutex;
|
||||
static int g_sqlite_count GUARDED_BY(g_sqlite_mutex) = 0;
|
||||
|
||||
static void ErrorLogCallback(void* arg, int code, const char* msg)
|
||||
{
|
||||
// From sqlite3_config() documentation for the SQLITE_CONFIG_LOG option:
|
||||
// "The void pointer that is the second argument to SQLITE_CONFIG_LOG is passed through as
|
||||
// the first parameter to the application-defined logger function whenever that function is
|
||||
// invoked."
|
||||
// Assert that this is the case:
|
||||
assert(arg == nullptr);
|
||||
LogPrintf("SQLite Error. Code: %d. Message: %s\n", code, msg);
|
||||
}
|
||||
|
||||
static std::optional<int> ReadPragmaInteger(sqlite3* db, const std::string& key, const std::string& description, bilingual_str& error)
|
||||
{
|
||||
std::string stmt_text = strprintf("PRAGMA %s", key);
|
||||
sqlite3_stmt* pragma_read_stmt{nullptr};
|
||||
int ret = sqlite3_prepare_v2(db, stmt_text.c_str(), -1, &pragma_read_stmt, nullptr);
|
||||
if (ret != SQLITE_OK) {
|
||||
sqlite3_finalize(pragma_read_stmt);
|
||||
error = Untranslated(strprintf("SQLiteDatabase: Failed to prepare the statement to fetch %s: %s", description, sqlite3_errstr(ret)));
|
||||
return std::nullopt;
|
||||
}
|
||||
ret = sqlite3_step(pragma_read_stmt);
|
||||
if (ret != SQLITE_ROW) {
|
||||
sqlite3_finalize(pragma_read_stmt);
|
||||
error = Untranslated(strprintf("SQLiteDatabase: Failed to fetch %s: %s", description, sqlite3_errstr(ret)));
|
||||
return std::nullopt;
|
||||
}
|
||||
int result = sqlite3_column_int(pragma_read_stmt, 0);
|
||||
sqlite3_finalize(pragma_read_stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void SetPragma(sqlite3* db, const std::string& key, const std::string& value, const std::string& err_msg)
|
||||
{
|
||||
std::string stmt_text = strprintf("PRAGMA %s = %s", key, value);
|
||||
int ret = sqlite3_exec(db, stmt_text.c_str(), nullptr, nullptr, nullptr);
|
||||
if (ret != SQLITE_OK) {
|
||||
throw std::runtime_error(strprintf("SQLiteDatabase: %s: %s\n", err_msg, sqlite3_errstr(ret)));
|
||||
}
|
||||
}
|
||||
|
||||
SQLiteDatabase::SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, bool mock)
|
||||
: WalletDatabase(), m_mock(mock), m_dir_path(dir_path.string()), m_file_path(file_path.string())
|
||||
{
|
||||
{
|
||||
LOCK(g_sqlite_mutex);
|
||||
LogPrintf("Using SQLite Version %s\n", SQLiteDatabaseVersion());
|
||||
LogPrintf("Using wallet %s\n", m_dir_path);
|
||||
|
||||
if (++g_sqlite_count == 1) {
|
||||
// Setup logging
|
||||
int ret = sqlite3_config(SQLITE_CONFIG_LOG, ErrorLogCallback, nullptr);
|
||||
if (ret != SQLITE_OK) {
|
||||
throw std::runtime_error(strprintf("SQLiteDatabase: Failed to setup error log: %s\n", sqlite3_errstr(ret)));
|
||||
}
|
||||
// Force serialized threading mode
|
||||
ret = sqlite3_config(SQLITE_CONFIG_SERIALIZED);
|
||||
if (ret != SQLITE_OK) {
|
||||
throw std::runtime_error(strprintf("SQLiteDatabase: Failed to configure serialized threading mode: %s\n", sqlite3_errstr(ret)));
|
||||
}
|
||||
}
|
||||
int ret = sqlite3_initialize(); // This is a no-op if sqlite3 is already initialized
|
||||
if (ret != SQLITE_OK) {
|
||||
throw std::runtime_error(strprintf("SQLiteDatabase: Failed to initialize SQLite: %s\n", sqlite3_errstr(ret)));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Open();
|
||||
} catch (const std::runtime_error&) {
|
||||
// If open fails, cleanup this object and rethrow the exception
|
||||
Cleanup();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void SQLiteBatch::SetupSQLStatements()
|
||||
{
|
||||
const std::vector<std::pair<sqlite3_stmt**, const char*>> statements{
|
||||
{&m_read_stmt, "SELECT value FROM main WHERE key = ?"},
|
||||
{&m_insert_stmt, "INSERT INTO main VALUES(?, ?)"},
|
||||
{&m_overwrite_stmt, "INSERT or REPLACE into main values(?, ?)"},
|
||||
{&m_delete_stmt, "DELETE FROM main WHERE key = ?"},
|
||||
{&m_cursor_stmt, "SELECT key, value FROM main"},
|
||||
};
|
||||
|
||||
for (const auto& [stmt_prepared, stmt_text] : statements) {
|
||||
if (*stmt_prepared == nullptr) {
|
||||
int res = sqlite3_prepare_v2(m_database.m_db, stmt_text, -1, stmt_prepared, nullptr);
|
||||
if (res != SQLITE_OK) {
|
||||
throw std::runtime_error(strprintf(
|
||||
"SQLiteDatabase: Failed to setup SQL statements: %s\n", sqlite3_errstr(res)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SQLiteDatabase::~SQLiteDatabase()
|
||||
{
|
||||
Cleanup();
|
||||
}
|
||||
|
||||
void SQLiteDatabase::Cleanup() noexcept
|
||||
{
|
||||
Close();
|
||||
|
||||
LOCK(g_sqlite_mutex);
|
||||
if (--g_sqlite_count == 0) {
|
||||
int ret = sqlite3_shutdown();
|
||||
if (ret != SQLITE_OK) {
|
||||
LogPrintf("SQLiteDatabase: Failed to shutdown SQLite: %s\n", sqlite3_errstr(ret));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::Verify(bilingual_str& error)
|
||||
{
|
||||
assert(m_db);
|
||||
|
||||
// Check the application ID matches our network magic
|
||||
auto read_result = ReadPragmaInteger(m_db, "application_id", "the application id", error);
|
||||
if (!read_result.has_value()) return false;
|
||||
uint32_t app_id = static_cast<uint32_t>(read_result.value());
|
||||
uint32_t net_magic = ReadBE32(Params().MessageStart());
|
||||
if (app_id != net_magic) {
|
||||
error = strprintf(_("SQLiteDatabase: Unexpected application id. Expected %u, got %u"), net_magic, app_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check our schema version
|
||||
read_result = ReadPragmaInteger(m_db, "user_version", "sqlite wallet schema version", error);
|
||||
if (!read_result.has_value()) return false;
|
||||
int32_t user_ver = read_result.value();
|
||||
if (user_ver != WALLET_SCHEMA_VERSION) {
|
||||
error = strprintf(_("SQLiteDatabase: Unknown sqlite wallet schema version %d. Only version %d is supported"), user_ver, WALLET_SCHEMA_VERSION);
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_stmt* stmt{nullptr};
|
||||
int ret = sqlite3_prepare_v2(m_db, "PRAGMA integrity_check", -1, &stmt, nullptr);
|
||||
if (ret != SQLITE_OK) {
|
||||
sqlite3_finalize(stmt);
|
||||
error = strprintf(_("SQLiteDatabase: Failed to prepare statement to verify database: %s"), sqlite3_errstr(ret));
|
||||
return false;
|
||||
}
|
||||
while (true) {
|
||||
ret = sqlite3_step(stmt);
|
||||
if (ret == SQLITE_DONE) {
|
||||
break;
|
||||
}
|
||||
if (ret != SQLITE_ROW) {
|
||||
error = strprintf(_("SQLiteDatabase: Failed to execute statement to verify database: %s"), sqlite3_errstr(ret));
|
||||
break;
|
||||
}
|
||||
const char* msg = (const char*)sqlite3_column_text(stmt, 0);
|
||||
if (!msg) {
|
||||
error = strprintf(_("SQLiteDatabase: Failed to read database verification error: %s"), sqlite3_errstr(ret));
|
||||
break;
|
||||
}
|
||||
std::string str_msg(msg);
|
||||
if (str_msg == "ok") {
|
||||
continue;
|
||||
}
|
||||
if (error.empty()) {
|
||||
error = _("Failed to verify database") + Untranslated("\n");
|
||||
}
|
||||
error += Untranslated(strprintf("%s\n", str_msg));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return error.empty();
|
||||
}
|
||||
|
||||
void SQLiteDatabase::Open()
|
||||
{
|
||||
int flags = SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
|
||||
if (m_mock) {
|
||||
flags |= SQLITE_OPEN_MEMORY; // In memory database for mock db
|
||||
}
|
||||
|
||||
if (m_db == nullptr) {
|
||||
if (!m_mock) {
|
||||
TryCreateDirectories(m_dir_path);
|
||||
}
|
||||
int ret = sqlite3_open_v2(m_file_path.c_str(), &m_db, flags, nullptr);
|
||||
if (ret != SQLITE_OK) {
|
||||
throw std::runtime_error(strprintf("SQLiteDatabase: Failed to open database: %s\n", sqlite3_errstr(ret)));
|
||||
}
|
||||
}
|
||||
|
||||
if (sqlite3_db_readonly(m_db, "main") != 0) {
|
||||
throw std::runtime_error("SQLiteDatabase: Database opened in readonly mode but read-write permissions are needed");
|
||||
}
|
||||
|
||||
// Acquire an exclusive lock on the database
|
||||
// First change the locking mode to exclusive
|
||||
SetPragma(m_db, "locking_mode", "exclusive", "Unable to change database locking mode to exclusive");
|
||||
// Now begin a transaction to acquire the exclusive lock. This lock won't be released until we close because of the exclusive locking mode.
|
||||
int ret = sqlite3_exec(m_db, "BEGIN EXCLUSIVE TRANSACTION", nullptr, nullptr, nullptr);
|
||||
if (ret != SQLITE_OK) {
|
||||
throw std::runtime_error("SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another instance of Vexta Core?\n");
|
||||
}
|
||||
ret = sqlite3_exec(m_db, "COMMIT", nullptr, nullptr, nullptr);
|
||||
if (ret != SQLITE_OK) {
|
||||
throw std::runtime_error(strprintf("SQLiteDatabase: Unable to end exclusive lock transaction: %s\n", sqlite3_errstr(ret)));
|
||||
}
|
||||
|
||||
// Enable fullfsync for the platforms that use it
|
||||
SetPragma(m_db, "fullfsync", "true", "Failed to enable fullfsync");
|
||||
|
||||
if (gArgs.GetBoolArg("-unsafesqlitesync", false)) {
|
||||
// Use normal synchronous mode for the journal
|
||||
LogPrintf("WARNING SQLite is configured to not wait for data to be flushed to disk. Data loss and corruption may occur.\n");
|
||||
SetPragma(m_db, "synchronous", "OFF", "Failed to set synchronous mode to OFF");
|
||||
}
|
||||
|
||||
// Make the table for our key-value pairs
|
||||
// First check that the main table exists
|
||||
sqlite3_stmt* check_main_stmt{nullptr};
|
||||
ret = sqlite3_prepare_v2(m_db, "SELECT name FROM sqlite_master WHERE type='table' AND name='main'", -1, &check_main_stmt, nullptr);
|
||||
if (ret != SQLITE_OK) {
|
||||
throw std::runtime_error(strprintf("SQLiteDatabase: Failed to prepare statement to check table existence: %s\n", sqlite3_errstr(ret)));
|
||||
}
|
||||
ret = sqlite3_step(check_main_stmt);
|
||||
if (sqlite3_finalize(check_main_stmt) != SQLITE_OK) {
|
||||
throw std::runtime_error(strprintf("SQLiteDatabase: Failed to finalize statement checking table existence: %s\n", sqlite3_errstr(ret)));
|
||||
}
|
||||
bool table_exists;
|
||||
if (ret == SQLITE_DONE) {
|
||||
table_exists = false;
|
||||
} else if (ret == SQLITE_ROW) {
|
||||
table_exists = true;
|
||||
} else {
|
||||
throw std::runtime_error(strprintf("SQLiteDatabase: Failed to execute statement to check table existence: %s\n", sqlite3_errstr(ret)));
|
||||
}
|
||||
|
||||
// Do the db setup things because the table doesn't exist only when we are creating a new wallet
|
||||
if (!table_exists) {
|
||||
ret = sqlite3_exec(m_db, "CREATE TABLE main(key BLOB PRIMARY KEY NOT NULL, value BLOB NOT NULL)", nullptr, nullptr, nullptr);
|
||||
if (ret != SQLITE_OK) {
|
||||
throw std::runtime_error(strprintf("SQLiteDatabase: Failed to create new database: %s\n", sqlite3_errstr(ret)));
|
||||
}
|
||||
|
||||
// Set the application id
|
||||
uint32_t app_id = ReadBE32(Params().MessageStart());
|
||||
SetPragma(m_db, "application_id", strprintf("%d", static_cast<int32_t>(app_id)),
|
||||
"Failed to set the application id");
|
||||
|
||||
// Set the user version
|
||||
SetPragma(m_db, "user_version", strprintf("%d", WALLET_SCHEMA_VERSION),
|
||||
"Failed to set the wallet schema version");
|
||||
}
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::Rewrite(const char* skip)
|
||||
{
|
||||
// Rewrite the database using the VACUUM command: https://sqlite.org/lang_vacuum.html
|
||||
int ret = sqlite3_exec(m_db, "VACUUM", nullptr, nullptr, nullptr);
|
||||
return ret == SQLITE_OK;
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::Backup(const std::string& dest) const
|
||||
{
|
||||
sqlite3* db_copy;
|
||||
int res = sqlite3_open(dest.c_str(), &db_copy);
|
||||
if (res != SQLITE_OK) {
|
||||
sqlite3_close(db_copy);
|
||||
return false;
|
||||
}
|
||||
sqlite3_backup* backup = sqlite3_backup_init(db_copy, "main", m_db, "main");
|
||||
if (!backup) {
|
||||
LogPrintf("%s: Unable to begin backup: %s\n", __func__, sqlite3_errmsg(m_db));
|
||||
sqlite3_close(db_copy);
|
||||
return false;
|
||||
}
|
||||
// Specifying -1 will copy all of the pages
|
||||
res = sqlite3_backup_step(backup, -1);
|
||||
if (res != SQLITE_DONE) {
|
||||
LogPrintf("%s: Unable to backup: %s\n", __func__, sqlite3_errstr(res));
|
||||
sqlite3_backup_finish(backup);
|
||||
sqlite3_close(db_copy);
|
||||
return false;
|
||||
}
|
||||
res = sqlite3_backup_finish(backup);
|
||||
sqlite3_close(db_copy);
|
||||
return res == SQLITE_OK;
|
||||
}
|
||||
|
||||
void SQLiteDatabase::Close()
|
||||
{
|
||||
int res = sqlite3_close(m_db);
|
||||
if (res != SQLITE_OK) {
|
||||
throw std::runtime_error(strprintf("SQLiteDatabase: Failed to close database: %s\n", sqlite3_errstr(res)));
|
||||
}
|
||||
m_db = nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<DatabaseBatch> SQLiteDatabase::MakeBatch(bool flush_on_close)
|
||||
{
|
||||
// We ignore flush_on_close because we don't do manual flushing for SQLite
|
||||
return std::make_unique<SQLiteBatch>(*this);
|
||||
}
|
||||
|
||||
SQLiteBatch::SQLiteBatch(SQLiteDatabase& database)
|
||||
: m_database(database)
|
||||
{
|
||||
// Make sure we have a db handle
|
||||
assert(m_database.m_db);
|
||||
|
||||
SetupSQLStatements();
|
||||
}
|
||||
|
||||
void SQLiteBatch::Close()
|
||||
{
|
||||
// If m_db is in a transaction (i.e. not in autocommit mode), then abort the transaction in progress
|
||||
if (m_database.m_db && sqlite3_get_autocommit(m_database.m_db) == 0) {
|
||||
if (TxnAbort()) {
|
||||
LogPrintf("SQLiteBatch: Batch closed unexpectedly without the transaction being explicitly committed or aborted\n");
|
||||
} else {
|
||||
LogPrintf("SQLiteBatch: Batch closed and failed to abort transaction\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Free all of the prepared statements
|
||||
const std::vector<std::pair<sqlite3_stmt**, const char*>> statements{
|
||||
{&m_read_stmt, "read"},
|
||||
{&m_insert_stmt, "insert"},
|
||||
{&m_overwrite_stmt, "overwrite"},
|
||||
{&m_delete_stmt, "delete"},
|
||||
{&m_cursor_stmt, "cursor"},
|
||||
};
|
||||
|
||||
for (const auto& [stmt_prepared, stmt_description] : statements) {
|
||||
int res = sqlite3_finalize(*stmt_prepared);
|
||||
if (res != SQLITE_OK) {
|
||||
LogPrintf("SQLiteBatch: Batch closed but could not finalize %s statement: %s\n",
|
||||
stmt_description, sqlite3_errstr(res));
|
||||
}
|
||||
*stmt_prepared = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool SQLiteBatch::ReadKey(CDataStream&& key, CDataStream& value)
|
||||
{
|
||||
if (!m_database.m_db) return false;
|
||||
assert(m_read_stmt);
|
||||
|
||||
// Bind: leftmost parameter in statement is index 1
|
||||
int res = sqlite3_bind_blob(m_read_stmt, 1, key.data(), key.size(), SQLITE_STATIC);
|
||||
if (res != SQLITE_OK) {
|
||||
LogPrintf("%s: Unable to bind statement: %s\n", __func__, sqlite3_errstr(res));
|
||||
sqlite3_clear_bindings(m_read_stmt);
|
||||
sqlite3_reset(m_read_stmt);
|
||||
return false;
|
||||
}
|
||||
res = sqlite3_step(m_read_stmt);
|
||||
if (res != SQLITE_ROW) {
|
||||
if (res != SQLITE_DONE) {
|
||||
// SQLITE_DONE means "not found", don't log an error in that case.
|
||||
LogPrintf("%s: Unable to execute statement: %s\n", __func__, sqlite3_errstr(res));
|
||||
}
|
||||
sqlite3_clear_bindings(m_read_stmt);
|
||||
sqlite3_reset(m_read_stmt);
|
||||
return false;
|
||||
}
|
||||
// Leftmost column in result is index 0
|
||||
const char* data = reinterpret_cast<const char*>(sqlite3_column_blob(m_read_stmt, 0));
|
||||
int data_size = sqlite3_column_bytes(m_read_stmt, 0);
|
||||
value.write(data, data_size);
|
||||
|
||||
sqlite3_clear_bindings(m_read_stmt);
|
||||
sqlite3_reset(m_read_stmt);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::WriteKey(CDataStream&& key, CDataStream&& value, bool overwrite)
|
||||
{
|
||||
if (!m_database.m_db) return false;
|
||||
assert(m_insert_stmt && m_overwrite_stmt);
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
if (overwrite) {
|
||||
stmt = m_overwrite_stmt;
|
||||
} else {
|
||||
stmt = m_insert_stmt;
|
||||
}
|
||||
|
||||
// Bind: leftmost parameter in statement is index 1
|
||||
// Insert index 1 is key, 2 is value
|
||||
int res = sqlite3_bind_blob(stmt, 1, key.data(), key.size(), SQLITE_STATIC);
|
||||
if (res != SQLITE_OK) {
|
||||
LogPrintf("%s: Unable to bind key to statement: %s\n", __func__, sqlite3_errstr(res));
|
||||
sqlite3_clear_bindings(stmt);
|
||||
sqlite3_reset(stmt);
|
||||
return false;
|
||||
}
|
||||
res = sqlite3_bind_blob(stmt, 2, value.data(), value.size(), SQLITE_STATIC);
|
||||
if (res != SQLITE_OK) {
|
||||
LogPrintf("%s: Unable to bind value to statement: %s\n", __func__, sqlite3_errstr(res));
|
||||
sqlite3_clear_bindings(stmt);
|
||||
sqlite3_reset(stmt);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Execute
|
||||
res = sqlite3_step(stmt);
|
||||
sqlite3_clear_bindings(stmt);
|
||||
sqlite3_reset(stmt);
|
||||
if (res != SQLITE_DONE) {
|
||||
LogPrintf("%s: Unable to execute statement: %s\n", __func__, sqlite3_errstr(res));
|
||||
}
|
||||
return res == SQLITE_DONE;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::EraseKey(CDataStream&& key)
|
||||
{
|
||||
if (!m_database.m_db) return false;
|
||||
assert(m_delete_stmt);
|
||||
|
||||
// Bind: leftmost parameter in statement is index 1
|
||||
int res = sqlite3_bind_blob(m_delete_stmt, 1, key.data(), key.size(), SQLITE_STATIC);
|
||||
if (res != SQLITE_OK) {
|
||||
LogPrintf("%s: Unable to bind statement: %s\n", __func__, sqlite3_errstr(res));
|
||||
sqlite3_clear_bindings(m_delete_stmt);
|
||||
sqlite3_reset(m_delete_stmt);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Execute
|
||||
res = sqlite3_step(m_delete_stmt);
|
||||
sqlite3_clear_bindings(m_delete_stmt);
|
||||
sqlite3_reset(m_delete_stmt);
|
||||
if (res != SQLITE_DONE) {
|
||||
LogPrintf("%s: Unable to execute statement: %s\n", __func__, sqlite3_errstr(res));
|
||||
}
|
||||
return res == SQLITE_DONE;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::HasKey(CDataStream&& key)
|
||||
{
|
||||
if (!m_database.m_db) return false;
|
||||
assert(m_read_stmt);
|
||||
|
||||
// Bind: leftmost parameter in statement is index 1
|
||||
bool ret = false;
|
||||
int res = sqlite3_bind_blob(m_read_stmt, 1, key.data(), key.size(), SQLITE_STATIC);
|
||||
if (res == SQLITE_OK) {
|
||||
res = sqlite3_step(m_read_stmt);
|
||||
if (res == SQLITE_ROW) {
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
|
||||
sqlite3_clear_bindings(m_read_stmt);
|
||||
sqlite3_reset(m_read_stmt);
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::StartCursor()
|
||||
{
|
||||
assert(!m_cursor_init);
|
||||
if (!m_database.m_db) return false;
|
||||
m_cursor_init = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::ReadAtCursor(CDataStream& key, CDataStream& value, bool& complete)
|
||||
{
|
||||
complete = false;
|
||||
|
||||
if (!m_cursor_init) return false;
|
||||
|
||||
int res = sqlite3_step(m_cursor_stmt);
|
||||
if (res == SQLITE_DONE) {
|
||||
complete = true;
|
||||
return true;
|
||||
}
|
||||
if (res != SQLITE_ROW) {
|
||||
LogPrintf("SQLiteBatch::ReadAtCursor: Unable to execute cursor step: %s\n", sqlite3_errstr(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Leftmost column in result is index 0
|
||||
const char* key_data = reinterpret_cast<const char*>(sqlite3_column_blob(m_cursor_stmt, 0));
|
||||
int key_data_size = sqlite3_column_bytes(m_cursor_stmt, 0);
|
||||
key.write(key_data, key_data_size);
|
||||
const char* value_data = reinterpret_cast<const char*>(sqlite3_column_blob(m_cursor_stmt, 1));
|
||||
int value_data_size = sqlite3_column_bytes(m_cursor_stmt, 1);
|
||||
value.write(value_data, value_data_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SQLiteBatch::CloseCursor()
|
||||
{
|
||||
sqlite3_reset(m_cursor_stmt);
|
||||
m_cursor_init = false;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::TxnBegin()
|
||||
{
|
||||
if (!m_database.m_db || sqlite3_get_autocommit(m_database.m_db) == 0) return false;
|
||||
int res = sqlite3_exec(m_database.m_db, "BEGIN TRANSACTION", nullptr, nullptr, nullptr);
|
||||
if (res != SQLITE_OK) {
|
||||
LogPrintf("SQLiteBatch: Failed to begin the transaction\n");
|
||||
}
|
||||
return res == SQLITE_OK;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::TxnCommit()
|
||||
{
|
||||
if (!m_database.m_db || sqlite3_get_autocommit(m_database.m_db) != 0) return false;
|
||||
int res = sqlite3_exec(m_database.m_db, "COMMIT TRANSACTION", nullptr, nullptr, nullptr);
|
||||
if (res != SQLITE_OK) {
|
||||
LogPrintf("SQLiteBatch: Failed to commit the transaction\n");
|
||||
}
|
||||
return res == SQLITE_OK;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::TxnAbort()
|
||||
{
|
||||
if (!m_database.m_db || sqlite3_get_autocommit(m_database.m_db) != 0) return false;
|
||||
int res = sqlite3_exec(m_database.m_db, "ROLLBACK TRANSACTION", nullptr, nullptr, nullptr);
|
||||
if (res != SQLITE_OK) {
|
||||
LogPrintf("SQLiteBatch: Failed to abort the transaction\n");
|
||||
}
|
||||
return res == SQLITE_OK;
|
||||
}
|
||||
|
||||
std::unique_ptr<SQLiteDatabase> MakeSQLiteDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error)
|
||||
{
|
||||
try {
|
||||
fs::path data_file = SQLiteDataFile(path);
|
||||
auto db = std::make_unique<SQLiteDatabase>(data_file.parent_path(), data_file);
|
||||
if (options.verify && !db->Verify(error)) {
|
||||
status = DatabaseStatus::FAILED_VERIFY;
|
||||
return nullptr;
|
||||
}
|
||||
status = DatabaseStatus::SUCCESS;
|
||||
return db;
|
||||
} catch (const std::runtime_error& e) {
|
||||
status = DatabaseStatus::FAILED_LOAD;
|
||||
error = Untranslated(e.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::string SQLiteDatabaseVersion()
|
||||
{
|
||||
return std::string(sqlite3_libversion());
|
||||
}
|
||||
120
src/wallet/sqlite.h
Normal file
120
src/wallet/sqlite.h
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
// Copyright (c) 2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_SQLITE_H
|
||||
#define DIGIBYTE_WALLET_SQLITE_H
|
||||
|
||||
#include <wallet/db.h>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
struct bilingual_str;
|
||||
class SQLiteDatabase;
|
||||
|
||||
/** RAII class that provides access to a WalletDatabase */
|
||||
class SQLiteBatch : public DatabaseBatch
|
||||
{
|
||||
private:
|
||||
SQLiteDatabase& m_database;
|
||||
|
||||
bool m_cursor_init = false;
|
||||
|
||||
sqlite3_stmt* m_read_stmt{nullptr};
|
||||
sqlite3_stmt* m_insert_stmt{nullptr};
|
||||
sqlite3_stmt* m_overwrite_stmt{nullptr};
|
||||
sqlite3_stmt* m_delete_stmt{nullptr};
|
||||
sqlite3_stmt* m_cursor_stmt{nullptr};
|
||||
|
||||
void SetupSQLStatements();
|
||||
|
||||
bool ReadKey(CDataStream&& key, CDataStream& value) override;
|
||||
bool WriteKey(CDataStream&& key, CDataStream&& value, bool overwrite = true) override;
|
||||
bool EraseKey(CDataStream&& key) override;
|
||||
bool HasKey(CDataStream&& key) override;
|
||||
|
||||
public:
|
||||
explicit SQLiteBatch(SQLiteDatabase& database);
|
||||
~SQLiteBatch() override { Close(); }
|
||||
|
||||
/* No-op. See comment on SQLiteDatabase::Flush */
|
||||
void Flush() override {}
|
||||
|
||||
void Close() override;
|
||||
|
||||
bool StartCursor() override;
|
||||
bool ReadAtCursor(CDataStream& key, CDataStream& value, bool& complete) override;
|
||||
void CloseCursor() override;
|
||||
bool TxnBegin() override;
|
||||
bool TxnCommit() override;
|
||||
bool TxnAbort() override;
|
||||
};
|
||||
|
||||
/** An instance of this class represents one SQLite3 database.
|
||||
**/
|
||||
class SQLiteDatabase : public WalletDatabase
|
||||
{
|
||||
private:
|
||||
const bool m_mock{false};
|
||||
|
||||
const std::string m_dir_path;
|
||||
|
||||
const std::string m_file_path;
|
||||
|
||||
void Cleanup() noexcept;
|
||||
|
||||
public:
|
||||
SQLiteDatabase() = delete;
|
||||
|
||||
/** Create DB handle to real database */
|
||||
SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, bool mock = false);
|
||||
|
||||
~SQLiteDatabase();
|
||||
|
||||
bool Verify(bilingual_str& error);
|
||||
|
||||
/** Open the database if it is not already opened */
|
||||
void Open() override;
|
||||
|
||||
/** Close the database */
|
||||
void Close() override;
|
||||
|
||||
/* These functions are unused */
|
||||
void AddRef() override { assert(false); }
|
||||
void RemoveRef() override { assert(false); }
|
||||
|
||||
/** Rewrite the entire database on disk */
|
||||
bool Rewrite(const char* skip = nullptr) override;
|
||||
|
||||
/** Back up the entire database to a file.
|
||||
*/
|
||||
bool Backup(const std::string& dest) const override;
|
||||
|
||||
/** No-ops
|
||||
*
|
||||
* SQLite always flushes everything to the database file after each transaction
|
||||
* (each Read/Write/Erase that we do is its own transaction unless we called
|
||||
* TxnBegin) so there is no need to have Flush or Periodic Flush.
|
||||
*
|
||||
* There is no DB env to reload, so ReloadDbEnv has nothing to do
|
||||
*/
|
||||
void Flush() override {}
|
||||
bool PeriodicFlush() override { return false; }
|
||||
void ReloadDbEnv() override {}
|
||||
|
||||
void IncrementUpdateCounter() override { ++nUpdateCounter; }
|
||||
|
||||
std::string Filename() override { return m_file_path; }
|
||||
std::string Format() override { return "sqlite"; }
|
||||
|
||||
/** Make a SQLiteBatch connected to this database */
|
||||
std::unique_ptr<DatabaseBatch> MakeBatch(bool flush_on_close = true) override;
|
||||
|
||||
sqlite3* m_db{nullptr};
|
||||
};
|
||||
|
||||
std::unique_ptr<SQLiteDatabase> MakeSQLiteDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error);
|
||||
|
||||
std::string SQLiteDatabaseVersion();
|
||||
|
||||
#endif // DIGIBYTE_WALLET_SQLITE_H
|
||||
137
src/wallet/test/accounting_tests.cpp
Normal file
137
src/wallet/test/accounting_tests.cpp
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
// Copyright (c) 2009-2019 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2019 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
#include <wallet/test/wallet_test_fixture.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(accounting_tests, WalletTestingSetup)
|
||||
|
||||
static void
|
||||
GetResults(CWallet& wallet, std::map<CAmount, CAccountingEntry>& results)
|
||||
{
|
||||
std::list<CAccountingEntry> aes;
|
||||
|
||||
results.clear();
|
||||
BOOST_CHECK(wallet.ReorderTransactions() == DBErrors::LOAD_OK);
|
||||
wallet.ListAccountCreditDebit("", aes);
|
||||
for (CAccountingEntry& ae : aes)
|
||||
{
|
||||
results[ae.nOrderPos] = ae;
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(acc_orderupgrade)
|
||||
{
|
||||
std::vector<CWalletTx*> vpwtx;
|
||||
CWalletTx wtx(nullptr /* pwallet */, MakeTransactionRef());
|
||||
CAccountingEntry ae;
|
||||
std::map<CAmount, CAccountingEntry> results;
|
||||
|
||||
LOCK(m_wallet.cs_wallet);
|
||||
|
||||
ae.strAccount = "";
|
||||
ae.nCreditDebit = 1;
|
||||
ae.nTime = 1333333333;
|
||||
ae.strOtherAccount = "b";
|
||||
ae.strComment = "";
|
||||
m_wallet.AddAccountingEntry(ae);
|
||||
|
||||
wtx.mapValue["comment"] = "z";
|
||||
m_wallet.AddToWallet(wtx);
|
||||
vpwtx.push_back(&m_wallet.mapWallet.at(wtx.GetHash()));
|
||||
vpwtx[0]->nTimeReceived = (unsigned int)1333333335;
|
||||
vpwtx[0]->nOrderPos = -1;
|
||||
|
||||
ae.nTime = 1333333336;
|
||||
ae.strOtherAccount = "c";
|
||||
m_wallet.AddAccountingEntry(ae);
|
||||
|
||||
GetResults(m_wallet, results);
|
||||
|
||||
BOOST_CHECK(m_wallet.nOrderPosNext == 3);
|
||||
BOOST_CHECK(2 == results.size());
|
||||
BOOST_CHECK(results[0].nTime == 1333333333);
|
||||
BOOST_CHECK(results[0].strComment.empty());
|
||||
BOOST_CHECK(1 == vpwtx[0]->nOrderPos);
|
||||
BOOST_CHECK(results[2].nTime == 1333333336);
|
||||
BOOST_CHECK(results[2].strOtherAccount == "c");
|
||||
|
||||
|
||||
ae.nTime = 1333333330;
|
||||
ae.strOtherAccount = "d";
|
||||
ae.nOrderPos = m_wallet.IncOrderPosNext();
|
||||
m_wallet.AddAccountingEntry(ae);
|
||||
|
||||
GetResults(m_wallet, results);
|
||||
|
||||
BOOST_CHECK(results.size() == 3);
|
||||
BOOST_CHECK(m_wallet.nOrderPosNext == 4);
|
||||
BOOST_CHECK(results[0].nTime == 1333333333);
|
||||
BOOST_CHECK(1 == vpwtx[0]->nOrderPos);
|
||||
BOOST_CHECK(results[2].nTime == 1333333336);
|
||||
BOOST_CHECK(results[3].nTime == 1333333330);
|
||||
BOOST_CHECK(results[3].strComment.empty());
|
||||
|
||||
|
||||
wtx.mapValue["comment"] = "y";
|
||||
{
|
||||
CMutableTransaction tx(*wtx.tx);
|
||||
++tx.nLockTime; // Just to change the hash :)
|
||||
wtx.SetTx(MakeTransactionRef(std::move(tx)));
|
||||
}
|
||||
m_wallet.AddToWallet(wtx);
|
||||
vpwtx.push_back(&m_wallet.mapWallet.at(wtx.GetHash()));
|
||||
vpwtx[1]->nTimeReceived = (unsigned int)1333333336;
|
||||
|
||||
wtx.mapValue["comment"] = "x";
|
||||
{
|
||||
CMutableTransaction tx(*wtx.tx);
|
||||
++tx.nLockTime; // Just to change the hash :)
|
||||
wtx.SetTx(MakeTransactionRef(std::move(tx)));
|
||||
}
|
||||
m_wallet.AddToWallet(wtx);
|
||||
vpwtx.push_back(&m_wallet.mapWallet.at(wtx.GetHash()));
|
||||
vpwtx[2]->nTimeReceived = (unsigned int)1333333329;
|
||||
vpwtx[2]->nOrderPos = -1;
|
||||
|
||||
GetResults(m_wallet, results);
|
||||
|
||||
BOOST_CHECK(results.size() == 3);
|
||||
BOOST_CHECK(m_wallet.nOrderPosNext == 6);
|
||||
BOOST_CHECK(0 == vpwtx[2]->nOrderPos);
|
||||
BOOST_CHECK(results[1].nTime == 1333333333);
|
||||
BOOST_CHECK(2 == vpwtx[0]->nOrderPos);
|
||||
BOOST_CHECK(results[3].nTime == 1333333336);
|
||||
BOOST_CHECK(results[4].nTime == 1333333330);
|
||||
BOOST_CHECK(results[4].strComment.empty());
|
||||
BOOST_CHECK(5 == vpwtx[1]->nOrderPos);
|
||||
|
||||
|
||||
ae.nTime = 1333333334;
|
||||
ae.strOtherAccount = "e";
|
||||
ae.nOrderPos = -1;
|
||||
m_wallet.AddAccountingEntry(ae);
|
||||
|
||||
GetResults(m_wallet, results);
|
||||
|
||||
BOOST_CHECK(results.size() == 4);
|
||||
BOOST_CHECK(m_wallet.nOrderPosNext == 7);
|
||||
BOOST_CHECK(0 == vpwtx[2]->nOrderPos);
|
||||
BOOST_CHECK(results[1].nTime == 1333333333);
|
||||
BOOST_CHECK(2 == vpwtx[0]->nOrderPos);
|
||||
BOOST_CHECK(results[3].nTime == 1333333336);
|
||||
BOOST_CHECK(results[3].strComment.empty());
|
||||
BOOST_CHECK(results[4].nTime == 1333333330);
|
||||
BOOST_CHECK(results[4].strComment.empty());
|
||||
BOOST_CHECK(results[5].nTime == 1333333334);
|
||||
BOOST_CHECK(6 == vpwtx[1]->nOrderPos);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
654
src/wallet/test/coinselector_tests.cpp
Normal file
654
src/wallet/test/coinselector_tests.cpp
Normal file
|
|
@ -0,0 +1,654 @@
|
|||
// Copyright (c) 2017-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2017-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <amount.h>
|
||||
#include <node/context.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <random.h>
|
||||
#include <test/util/setup_common.h>
|
||||
#include <wallet/coincontrol.h>
|
||||
#include <wallet/coinselection.h>
|
||||
#include <wallet/test/wallet_test_fixture.h>
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
#include <random>
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(coinselector_tests, WalletTestingSetup)
|
||||
|
||||
// how many times to run all the tests to have a chance to catch errors that only show up with particular random shuffles
|
||||
#define RUN_TESTS 100
|
||||
|
||||
// some tests fail 1% of the time due to bad luck.
|
||||
// we repeat those tests this many times and only complain if all iterations of the test fail
|
||||
#define RANDOM_REPEATS 5
|
||||
|
||||
typedef std::set<CInputCoin> CoinSet;
|
||||
|
||||
static std::vector<COutput> vCoins;
|
||||
static NodeContext testNode;
|
||||
static auto testChain = interfaces::MakeChain(testNode);
|
||||
static CWallet testWallet(testChain.get(), "", CreateDummyWalletDatabase());
|
||||
static CAmount balance = 0;
|
||||
|
||||
CoinEligibilityFilter filter_standard(1, 6, 0);
|
||||
CoinEligibilityFilter filter_confirmed(1, 1, 0);
|
||||
CoinEligibilityFilter filter_standard_extra(6, 6, 0);
|
||||
CoinSelectionParams coin_selection_params(/* change_output_size= */ 0,
|
||||
/* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(0),
|
||||
/* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
|
||||
/* tx_no_inputs_size= */ 0, /* avoid_partial= */ false);
|
||||
|
||||
static void add_coin(const CAmount& nValue, int nInput, std::vector<CInputCoin>& set)
|
||||
{
|
||||
CMutableTransaction tx;
|
||||
tx.vout.resize(nInput + 1);
|
||||
tx.vout[nInput].nValue = nValue;
|
||||
set.emplace_back(MakeTransactionRef(tx), nInput);
|
||||
}
|
||||
|
||||
static void add_coin(const CAmount& nValue, int nInput, CoinSet& set)
|
||||
{
|
||||
CMutableTransaction tx;
|
||||
tx.vout.resize(nInput + 1);
|
||||
tx.vout[nInput].nValue = nValue;
|
||||
set.emplace(MakeTransactionRef(tx), nInput);
|
||||
}
|
||||
|
||||
static void add_coin(CWallet& wallet, const CAmount& nValue, int nAge = 6*24, bool fIsFromMe = false, int nInput=0, bool spendable = false)
|
||||
{
|
||||
balance += nValue;
|
||||
static int nextLockTime = 0;
|
||||
CMutableTransaction tx;
|
||||
tx.nLockTime = nextLockTime++; // so all transactions get different hashes
|
||||
tx.vout.resize(nInput + 1);
|
||||
tx.vout[nInput].nValue = nValue;
|
||||
if (spendable) {
|
||||
CTxDestination dest;
|
||||
std::string error;
|
||||
const bool destination_ok = wallet.GetNewDestination(OutputType::BECH32, "", dest, error);
|
||||
assert(destination_ok);
|
||||
tx.vout[nInput].scriptPubKey = GetScriptForDestination(dest);
|
||||
}
|
||||
if (fIsFromMe) {
|
||||
// IsFromMe() returns (GetDebit() > 0), and GetDebit() is 0 if vin.empty(),
|
||||
// so stop vin being empty, and cache a non-zero Debit to fake out IsFromMe()
|
||||
tx.vin.resize(1);
|
||||
}
|
||||
CWalletTx* wtx = wallet.AddToWallet(MakeTransactionRef(std::move(tx)), /* confirm= */ {});
|
||||
if (fIsFromMe)
|
||||
{
|
||||
wtx->m_amounts[CWalletTx::DEBIT].Set(ISMINE_SPENDABLE, 1);
|
||||
wtx->m_is_cache_empty = false;
|
||||
}
|
||||
COutput output(wtx, nInput, nAge, true /* spendable */, true /* solvable */, true /* safe */);
|
||||
vCoins.push_back(output);
|
||||
}
|
||||
static void add_coin(const CAmount& nValue, int nAge = 6*24, bool fIsFromMe = false, int nInput=0, bool spendable = false)
|
||||
{
|
||||
add_coin(testWallet, nValue, nAge, fIsFromMe, nInput, spendable);
|
||||
}
|
||||
|
||||
static void empty_wallet(void)
|
||||
{
|
||||
vCoins.clear();
|
||||
balance = 0;
|
||||
}
|
||||
|
||||
static bool equal_sets(CoinSet a, CoinSet b)
|
||||
{
|
||||
std::pair<CoinSet::iterator, CoinSet::iterator> ret = mismatch(a.begin(), a.end(), b.begin());
|
||||
return ret.first == a.end() && ret.second == b.end();
|
||||
}
|
||||
|
||||
static CAmount make_hard_case(int utxos, std::vector<CInputCoin>& utxo_pool)
|
||||
{
|
||||
utxo_pool.clear();
|
||||
CAmount target = 0;
|
||||
for (int i = 0; i < utxos; ++i) {
|
||||
target += (CAmount)1 << (utxos+i);
|
||||
add_coin((CAmount)1 << (utxos+i), 2*i, utxo_pool);
|
||||
add_coin(((CAmount)1 << (utxos+i)) + ((CAmount)1 << (utxos-1-i)), 2*i + 1, utxo_pool);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
inline std::vector<OutputGroup>& GroupCoins(const std::vector<CInputCoin>& coins)
|
||||
{
|
||||
static std::vector<OutputGroup> static_groups;
|
||||
static_groups.clear();
|
||||
for (auto& coin : coins) {
|
||||
static_groups.emplace_back();
|
||||
static_groups.back().Insert(coin, 0, true, 0, 0, false);
|
||||
}
|
||||
return static_groups;
|
||||
}
|
||||
|
||||
inline std::vector<OutputGroup>& GroupCoins(const std::vector<COutput>& coins)
|
||||
{
|
||||
static std::vector<OutputGroup> static_groups;
|
||||
static_groups.clear();
|
||||
for (auto& coin : coins) {
|
||||
static_groups.emplace_back();
|
||||
static_groups.back().Insert(coin.GetInputCoin(), coin.nDepth, coin.tx->m_amounts[CWalletTx::DEBIT].m_cached[ISMINE_SPENDABLE] && coin.tx->m_amounts[CWalletTx::DEBIT].m_value[ISMINE_SPENDABLE] == 1 /* HACK: we can't figure out the is_me flag so we use the conditions defined above; perhaps set safe to false for !fIsFromMe in add_coin() */, 0, 0, false);
|
||||
}
|
||||
return static_groups;
|
||||
}
|
||||
|
||||
// Branch and bound coin selection tests
|
||||
BOOST_AUTO_TEST_CASE(bnb_search_test)
|
||||
{
|
||||
|
||||
LOCK(testWallet.cs_wallet);
|
||||
testWallet.SetupLegacyScriptPubKeyMan();
|
||||
|
||||
// Setup
|
||||
std::vector<CInputCoin> utxo_pool;
|
||||
CoinSet selection;
|
||||
CoinSet actual_selection;
|
||||
CAmount value_ret = 0;
|
||||
|
||||
/////////////////////////
|
||||
// Known Outcome tests //
|
||||
/////////////////////////
|
||||
|
||||
// Empty utxo pool
|
||||
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT, selection, value_ret));
|
||||
selection.clear();
|
||||
|
||||
// Add utxos
|
||||
add_coin(1 * CENT, 1, utxo_pool);
|
||||
add_coin(2 * CENT, 2, utxo_pool);
|
||||
add_coin(3 * CENT, 3, utxo_pool);
|
||||
add_coin(4 * CENT, 4, utxo_pool);
|
||||
|
||||
// Select 1 Cent
|
||||
add_coin(1 * CENT, 1, actual_selection);
|
||||
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT, selection, value_ret));
|
||||
BOOST_CHECK(equal_sets(selection, actual_selection));
|
||||
BOOST_CHECK_EQUAL(value_ret, 1 * CENT);
|
||||
actual_selection.clear();
|
||||
selection.clear();
|
||||
|
||||
// Select 2 Cent
|
||||
add_coin(2 * CENT, 2, actual_selection);
|
||||
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 2 * CENT, 0.5 * CENT, selection, value_ret));
|
||||
BOOST_CHECK(equal_sets(selection, actual_selection));
|
||||
BOOST_CHECK_EQUAL(value_ret, 2 * CENT);
|
||||
actual_selection.clear();
|
||||
selection.clear();
|
||||
|
||||
// Select 5 Cent
|
||||
add_coin(4 * CENT, 4, actual_selection);
|
||||
add_coin(1 * CENT, 1, actual_selection);
|
||||
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 5 * CENT, 0.5 * CENT, selection, value_ret));
|
||||
BOOST_CHECK(equal_sets(selection, actual_selection));
|
||||
BOOST_CHECK_EQUAL(value_ret, 5 * CENT);
|
||||
actual_selection.clear();
|
||||
selection.clear();
|
||||
|
||||
// Select 11 Cent, not possible
|
||||
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 11 * CENT, 0.5 * CENT, selection, value_ret));
|
||||
actual_selection.clear();
|
||||
selection.clear();
|
||||
|
||||
// Cost of change is greater than the difference between target value and utxo sum
|
||||
add_coin(1 * CENT, 1, actual_selection);
|
||||
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0.5 * CENT, selection, value_ret));
|
||||
BOOST_CHECK_EQUAL(value_ret, 1 * CENT);
|
||||
BOOST_CHECK(equal_sets(selection, actual_selection));
|
||||
actual_selection.clear();
|
||||
selection.clear();
|
||||
|
||||
// Cost of change is less than the difference between target value and utxo sum
|
||||
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0, selection, value_ret));
|
||||
actual_selection.clear();
|
||||
selection.clear();
|
||||
|
||||
// Select 10 Cent
|
||||
add_coin(5 * CENT, 5, utxo_pool);
|
||||
add_coin(5 * CENT, 5, actual_selection);
|
||||
add_coin(4 * CENT, 4, actual_selection);
|
||||
add_coin(1 * CENT, 1, actual_selection);
|
||||
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, 0.5 * CENT, selection, value_ret));
|
||||
BOOST_CHECK(equal_sets(selection, actual_selection));
|
||||
BOOST_CHECK_EQUAL(value_ret, 10 * CENT);
|
||||
actual_selection.clear();
|
||||
selection.clear();
|
||||
|
||||
// Negative effective value
|
||||
// Select 10 Cent but have 1 Cent not be possible because too small
|
||||
add_coin(5 * CENT, 5, actual_selection);
|
||||
add_coin(3 * CENT, 3, actual_selection);
|
||||
add_coin(2 * CENT, 2, actual_selection);
|
||||
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, 5000, selection, value_ret));
|
||||
BOOST_CHECK_EQUAL(value_ret, 10 * CENT);
|
||||
// FIXME: this test is redundant with the above, because 1 Cent is selected, not "too small"
|
||||
// BOOST_CHECK(equal_sets(selection, actual_selection));
|
||||
|
||||
// Select 0.25 Cent, not possible
|
||||
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.25 * CENT, 0.5 * CENT, selection, value_ret));
|
||||
actual_selection.clear();
|
||||
selection.clear();
|
||||
|
||||
// Iteration exhaustion test
|
||||
CAmount target = make_hard_case(17, utxo_pool);
|
||||
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), target, 0, selection, value_ret)); // Should exhaust
|
||||
target = make_hard_case(14, utxo_pool);
|
||||
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), target, 0, selection, value_ret)); // Should not exhaust
|
||||
|
||||
// Test same value early bailout optimization
|
||||
utxo_pool.clear();
|
||||
add_coin(7 * CENT, 7, actual_selection);
|
||||
add_coin(7 * CENT, 7, actual_selection);
|
||||
add_coin(7 * CENT, 7, actual_selection);
|
||||
add_coin(7 * CENT, 7, actual_selection);
|
||||
add_coin(2 * CENT, 7, actual_selection);
|
||||
add_coin(7 * CENT, 7, utxo_pool);
|
||||
add_coin(7 * CENT, 7, utxo_pool);
|
||||
add_coin(7 * CENT, 7, utxo_pool);
|
||||
add_coin(7 * CENT, 7, utxo_pool);
|
||||
add_coin(2 * CENT, 7, utxo_pool);
|
||||
for (int i = 0; i < 50000; ++i) {
|
||||
add_coin(5 * CENT, 7, utxo_pool);
|
||||
}
|
||||
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 30 * CENT, 5000, selection, value_ret));
|
||||
BOOST_CHECK_EQUAL(value_ret, 30 * CENT);
|
||||
BOOST_CHECK(equal_sets(selection, actual_selection));
|
||||
|
||||
////////////////////
|
||||
// Behavior tests //
|
||||
////////////////////
|
||||
// Select 1 Cent with pool of only greater than 5 Cent
|
||||
utxo_pool.clear();
|
||||
for (int i = 5; i <= 20; ++i) {
|
||||
add_coin(i * CENT, i, utxo_pool);
|
||||
}
|
||||
// Run 100 times, to make sure it is never finding a solution
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 2 * CENT, selection, value_ret));
|
||||
}
|
||||
|
||||
// Make sure that effective value is working in AttemptSelection when BnB is used
|
||||
CoinSelectionParams coin_selection_params_bnb(/* change_output_size= */ 0,
|
||||
/* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(3000),
|
||||
/* long_term_feerate= */ CFeeRate(1000), /* discard_feerate= */ CFeeRate(1000),
|
||||
/* tx_no_inputs_size= */ 0, /* avoid_partial= */ false);
|
||||
CoinSet setCoinsRet;
|
||||
CAmount nValueRet;
|
||||
empty_wallet();
|
||||
add_coin(1);
|
||||
vCoins.at(0).nInputBytes = 40; // Make sure that it has a negative effective value. The next check should assert if this somehow got through. Otherwise it will fail
|
||||
BOOST_CHECK(!testWallet.AttemptSelection( 1 * CENT, filter_standard, vCoins, setCoinsRet, nValueRet, coin_selection_params_bnb));
|
||||
|
||||
// Test fees subtracted from output:
|
||||
empty_wallet();
|
||||
add_coin(1 * CENT);
|
||||
vCoins.at(0).nInputBytes = 40;
|
||||
coin_selection_params_bnb.m_subtract_fee_outputs = true;
|
||||
BOOST_CHECK(testWallet.AttemptSelection( 1 * CENT, filter_standard, vCoins, setCoinsRet, nValueRet, coin_selection_params_bnb));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 1 * CENT);
|
||||
|
||||
// Make sure that can use BnB when there are preset inputs
|
||||
empty_wallet();
|
||||
{
|
||||
std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), "", CreateMockWalletDatabase());
|
||||
wallet->LoadWallet();
|
||||
wallet->SetupLegacyScriptPubKeyMan();
|
||||
LOCK(wallet->cs_wallet);
|
||||
add_coin(*wallet, 5 * CENT, 6 * 24, false, 0, true);
|
||||
add_coin(*wallet, 3 * CENT, 6 * 24, false, 0, true);
|
||||
add_coin(*wallet, 2 * CENT, 6 * 24, false, 0, true);
|
||||
CCoinControl coin_control;
|
||||
coin_control.fAllowOtherInputs = true;
|
||||
coin_control.Select(COutPoint(vCoins.at(0).tx->GetHash(), vCoins.at(0).i));
|
||||
coin_selection_params_bnb.m_effective_feerate = CFeeRate(0);
|
||||
BOOST_CHECK(wallet->SelectCoins(vCoins, 10 * CENT, setCoinsRet, nValueRet, coin_control, coin_selection_params_bnb));
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(knapsack_solver_test)
|
||||
{
|
||||
CoinSet setCoinsRet, setCoinsRet2;
|
||||
CAmount nValueRet;
|
||||
|
||||
LOCK(testWallet.cs_wallet);
|
||||
testWallet.SetupLegacyScriptPubKeyMan();
|
||||
|
||||
// test multiple times to allow for differences in the shuffle order
|
||||
for (int i = 0; i < RUN_TESTS; i++)
|
||||
{
|
||||
empty_wallet();
|
||||
|
||||
// with an empty wallet we can't even pay one cent
|
||||
BOOST_CHECK(!testWallet.AttemptSelection( 1 * CENT, filter_standard, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
|
||||
add_coin(1*CENT, 4); // add a new 1 cent coin
|
||||
|
||||
// with a new 1 cent coin, we still can't find a mature 1 cent
|
||||
BOOST_CHECK(!testWallet.AttemptSelection( 1 * CENT, filter_standard, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
|
||||
// but we can find a new 1 cent
|
||||
BOOST_CHECK( testWallet.AttemptSelection( 1 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 1 * CENT);
|
||||
|
||||
add_coin(2*CENT); // add a mature 2 cent coin
|
||||
|
||||
// we can't make 3 cents of mature coins
|
||||
BOOST_CHECK(!testWallet.AttemptSelection( 3 * CENT, filter_standard, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
|
||||
// we can make 3 cents of new coins
|
||||
BOOST_CHECK( testWallet.AttemptSelection( 3 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 3 * CENT);
|
||||
|
||||
add_coin(5*CENT); // add a mature 5 cent coin,
|
||||
add_coin(10*CENT, 3, true); // a new 10 cent coin sent from one of our own addresses
|
||||
add_coin(20*CENT); // and a mature 20 cent coin
|
||||
|
||||
// now we have new: 1+10=11 (of which 10 was self-sent), and mature: 2+5+20=27. total = 38
|
||||
|
||||
// we can't make 38 cents only if we disallow new coins:
|
||||
BOOST_CHECK(!testWallet.AttemptSelection(38 * CENT, filter_standard, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
// we can't even make 37 cents if we don't allow new coins even if they're from us
|
||||
BOOST_CHECK(!testWallet.AttemptSelection(38 * CENT, filter_standard_extra, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
// but we can make 37 cents if we accept new coins from ourself
|
||||
BOOST_CHECK( testWallet.AttemptSelection(37 * CENT, filter_standard, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 37 * CENT);
|
||||
// and we can make 38 cents if we accept all new coins
|
||||
BOOST_CHECK( testWallet.AttemptSelection(38 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 38 * CENT);
|
||||
|
||||
// try making 34 cents from 1,2,5,10,20 - we can't do it exactly
|
||||
BOOST_CHECK( testWallet.AttemptSelection(34 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 35 * CENT); // but 35 cents is closest
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 3U); // the best should be 20+10+5. it's incredibly unlikely the 1 or 2 got included (but possible)
|
||||
|
||||
// when we try making 7 cents, the smaller coins (1,2,5) are enough. We should see just 2+5
|
||||
BOOST_CHECK( testWallet.AttemptSelection( 7 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 7 * CENT);
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U);
|
||||
|
||||
// when we try making 8 cents, the smaller coins (1,2,5) are exactly enough.
|
||||
BOOST_CHECK( testWallet.AttemptSelection( 8 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK(nValueRet == 8 * CENT);
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 3U);
|
||||
|
||||
// when we try making 9 cents, no subset of smaller coins is enough, and we get the next bigger coin (10)
|
||||
BOOST_CHECK( testWallet.AttemptSelection( 9 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 10 * CENT);
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U);
|
||||
|
||||
// now clear out the wallet and start again to test choosing between subsets of smaller coins and the next biggest coin
|
||||
empty_wallet();
|
||||
|
||||
add_coin( 6*CENT);
|
||||
add_coin( 7*CENT);
|
||||
add_coin( 8*CENT);
|
||||
add_coin(20*CENT);
|
||||
add_coin(30*CENT); // now we have 6+7+8+20+30 = 71 cents total
|
||||
|
||||
// check that we have 71 and not 72
|
||||
BOOST_CHECK( testWallet.AttemptSelection(71 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK(!testWallet.AttemptSelection(72 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
|
||||
// now try making 16 cents. the best smaller coins can do is 6+7+8 = 21; not as good at the next biggest coin, 20
|
||||
BOOST_CHECK( testWallet.AttemptSelection(16 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 20 * CENT); // we should get 20 in one coin
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U);
|
||||
|
||||
add_coin( 5*CENT); // now we have 5+6+7+8+20+30 = 75 cents total
|
||||
|
||||
// now if we try making 16 cents again, the smaller coins can make 5+6+7 = 18 cents, better than the next biggest coin, 20
|
||||
BOOST_CHECK( testWallet.AttemptSelection(16 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 18 * CENT); // we should get 18 in 3 coins
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 3U);
|
||||
|
||||
add_coin( 18*CENT); // now we have 5+6+7+8+18+20+30
|
||||
|
||||
// and now if we try making 16 cents again, the smaller coins can make 5+6+7 = 18 cents, the same as the next biggest coin, 18
|
||||
BOOST_CHECK( testWallet.AttemptSelection(16 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 18 * CENT); // we should get 18 in 1 coin
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); // because in the event of a tie, the biggest coin wins
|
||||
|
||||
// now try making 11 cents. we should get 5+6
|
||||
BOOST_CHECK( testWallet.AttemptSelection(11 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 11 * CENT);
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U);
|
||||
|
||||
// check that the smallest bigger coin is used
|
||||
add_coin( 1*COIN);
|
||||
add_coin( 2*COIN);
|
||||
add_coin( 3*COIN);
|
||||
add_coin( 4*COIN); // now we have 5+6+7+8+18+20+30+100+200+300+400 = 1094 cents
|
||||
BOOST_CHECK( testWallet.AttemptSelection(95 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 1 * COIN); // we should get 1 DGB in 1 coin
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U);
|
||||
|
||||
BOOST_CHECK( testWallet.AttemptSelection(195 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 2 * COIN); // we should get 2 DGB in 1 coin
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U);
|
||||
|
||||
// empty the wallet and start again, now with fractions of a cent, to test small change avoidance
|
||||
|
||||
empty_wallet();
|
||||
add_coin(MIN_CHANGE * 1 / 10);
|
||||
add_coin(MIN_CHANGE * 2 / 10);
|
||||
add_coin(MIN_CHANGE * 3 / 10);
|
||||
add_coin(MIN_CHANGE * 4 / 10);
|
||||
add_coin(MIN_CHANGE * 5 / 10);
|
||||
|
||||
// try making 1 * MIN_CHANGE from the 1.5 * MIN_CHANGE
|
||||
// we'll get change smaller than MIN_CHANGE whatever happens, so can expect MIN_CHANGE exactly
|
||||
BOOST_CHECK( testWallet.AttemptSelection(MIN_CHANGE, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, MIN_CHANGE);
|
||||
|
||||
// but if we add a bigger coin, small change is avoided
|
||||
add_coin(1111*MIN_CHANGE);
|
||||
|
||||
// try making 1 from 0.1 + 0.2 + 0.3 + 0.4 + 0.5 + 1111 = 1112.5
|
||||
BOOST_CHECK( testWallet.AttemptSelection(1 * MIN_CHANGE, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 1 * MIN_CHANGE); // we should get the exact amount
|
||||
|
||||
// if we add more small coins:
|
||||
add_coin(MIN_CHANGE * 6 / 10);
|
||||
add_coin(MIN_CHANGE * 7 / 10);
|
||||
|
||||
// and try again to make 1.0 * MIN_CHANGE
|
||||
BOOST_CHECK( testWallet.AttemptSelection(1 * MIN_CHANGE, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 1 * MIN_CHANGE); // we should get the exact amount
|
||||
|
||||
// run the 'mtgox' test (see https://blockexplorer.com/tx/29a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf)
|
||||
// they tried to consolidate 10 50k coins into one 500k coin, and ended up with 50k in change
|
||||
empty_wallet();
|
||||
for (int j = 0; j < 20; j++)
|
||||
add_coin(50000 * COIN);
|
||||
|
||||
BOOST_CHECK( testWallet.AttemptSelection(500000 * COIN, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 500000 * COIN); // we should get the exact amount
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 10U); // in ten coins
|
||||
|
||||
// if there's not enough in the smaller coins to make at least 1 * MIN_CHANGE change (0.5+0.6+0.7 < 1.0+1.0),
|
||||
// we need to try finding an exact subset anyway
|
||||
|
||||
// sometimes it will fail, and so we use the next biggest coin:
|
||||
empty_wallet();
|
||||
add_coin(MIN_CHANGE * 5 / 10);
|
||||
add_coin(MIN_CHANGE * 6 / 10);
|
||||
add_coin(MIN_CHANGE * 7 / 10);
|
||||
add_coin(1111 * MIN_CHANGE);
|
||||
BOOST_CHECK( testWallet.AttemptSelection(1 * MIN_CHANGE, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 1111 * MIN_CHANGE); // we get the bigger coin
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U);
|
||||
|
||||
// but sometimes it's possible, and we use an exact subset (0.4 + 0.6 = 1.0)
|
||||
empty_wallet();
|
||||
add_coin(MIN_CHANGE * 4 / 10);
|
||||
add_coin(MIN_CHANGE * 6 / 10);
|
||||
add_coin(MIN_CHANGE * 8 / 10);
|
||||
add_coin(1111 * MIN_CHANGE);
|
||||
BOOST_CHECK( testWallet.AttemptSelection(MIN_CHANGE, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, MIN_CHANGE); // we should get the exact amount
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); // in two coins 0.4+0.6
|
||||
|
||||
// test avoiding small change
|
||||
empty_wallet();
|
||||
add_coin(MIN_CHANGE * 5 / 100);
|
||||
add_coin(MIN_CHANGE * 1);
|
||||
add_coin(MIN_CHANGE * 100);
|
||||
|
||||
// trying to make 100.01 from these three coins
|
||||
BOOST_CHECK(testWallet.AttemptSelection(MIN_CHANGE * 10001 / 100, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, MIN_CHANGE * 10105 / 100); // we should get all coins
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 3U);
|
||||
|
||||
// but if we try to make 99.9, we should take the bigger of the two small coins to avoid small change
|
||||
BOOST_CHECK(testWallet.AttemptSelection(MIN_CHANGE * 9990 / 100, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 101 * MIN_CHANGE);
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U);
|
||||
}
|
||||
|
||||
// test with many inputs
|
||||
for (CAmount amt=1500; amt < COIN; amt*=10) {
|
||||
empty_wallet();
|
||||
// Create 676 inputs (= (old MAX_STANDARD_TX_SIZE == 100000) / 148 bytes per input)
|
||||
for (uint16_t j = 0; j < 676; j++)
|
||||
add_coin(amt);
|
||||
|
||||
// We only create the wallet once to save time, but we still run the coin selection RUN_TESTS times.
|
||||
for (int i = 0; i < RUN_TESTS; i++) {
|
||||
BOOST_CHECK(testWallet.AttemptSelection(2000, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
|
||||
if (amt - 2000 < MIN_CHANGE) {
|
||||
// needs more than one input:
|
||||
uint16_t returnSize = std::ceil((2000.0 + MIN_CHANGE)/amt);
|
||||
CAmount returnValue = amt * returnSize;
|
||||
BOOST_CHECK_EQUAL(nValueRet, returnValue);
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), returnSize);
|
||||
} else {
|
||||
// one input is sufficient:
|
||||
BOOST_CHECK_EQUAL(nValueRet, amt);
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// test randomness
|
||||
{
|
||||
empty_wallet();
|
||||
for (int i2 = 0; i2 < 100; i2++)
|
||||
add_coin(COIN);
|
||||
|
||||
// Again, we only create the wallet once to save time, but we still run the coin selection RUN_TESTS times.
|
||||
for (int i = 0; i < RUN_TESTS; i++) {
|
||||
// picking 50 from 100 coins doesn't depend on the shuffle,
|
||||
// but does depend on randomness in the stochastic approximation code
|
||||
BOOST_CHECK(KnapsackSolver(50 * COIN, GroupCoins(vCoins), setCoinsRet, nValueRet));
|
||||
BOOST_CHECK(KnapsackSolver(50 * COIN, GroupCoins(vCoins), setCoinsRet2, nValueRet));
|
||||
BOOST_CHECK(!equal_sets(setCoinsRet, setCoinsRet2));
|
||||
|
||||
int fails = 0;
|
||||
for (int j = 0; j < RANDOM_REPEATS; j++)
|
||||
{
|
||||
// Test that the KnapsackSolver selects randomly from equivalent coins (same value and same input size).
|
||||
// When choosing 1 from 100 identical coins, 1% of the time, this test will choose the same coin twice
|
||||
// which will cause it to fail.
|
||||
// To avoid that issue, run the test RANDOM_REPEATS times and only complain if all of them fail
|
||||
BOOST_CHECK(KnapsackSolver(COIN, GroupCoins(vCoins), setCoinsRet, nValueRet));
|
||||
BOOST_CHECK(KnapsackSolver(COIN, GroupCoins(vCoins), setCoinsRet2, nValueRet));
|
||||
if (equal_sets(setCoinsRet, setCoinsRet2))
|
||||
fails++;
|
||||
}
|
||||
BOOST_CHECK_NE(fails, RANDOM_REPEATS);
|
||||
}
|
||||
|
||||
// add 75 cents in small change. not enough to make 90 cents,
|
||||
// then try making 90 cents. there are multiple competing "smallest bigger" coins,
|
||||
// one of which should be picked at random
|
||||
add_coin(5 * CENT);
|
||||
add_coin(10 * CENT);
|
||||
add_coin(15 * CENT);
|
||||
add_coin(20 * CENT);
|
||||
add_coin(25 * CENT);
|
||||
|
||||
for (int i = 0; i < RUN_TESTS; i++) {
|
||||
int fails = 0;
|
||||
for (int j = 0; j < RANDOM_REPEATS; j++)
|
||||
{
|
||||
BOOST_CHECK(KnapsackSolver(90*CENT, GroupCoins(vCoins), setCoinsRet, nValueRet));
|
||||
BOOST_CHECK(KnapsackSolver(90*CENT, GroupCoins(vCoins), setCoinsRet2, nValueRet));
|
||||
if (equal_sets(setCoinsRet, setCoinsRet2))
|
||||
fails++;
|
||||
}
|
||||
BOOST_CHECK_NE(fails, RANDOM_REPEATS);
|
||||
}
|
||||
}
|
||||
|
||||
empty_wallet();
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(ApproximateBestSubset)
|
||||
{
|
||||
CoinSet setCoinsRet;
|
||||
CAmount nValueRet;
|
||||
|
||||
LOCK(testWallet.cs_wallet);
|
||||
testWallet.SetupLegacyScriptPubKeyMan();
|
||||
|
||||
empty_wallet();
|
||||
|
||||
// Test vValue sort order
|
||||
for (int i = 0; i < 1000; i++)
|
||||
add_coin(1000 * COIN);
|
||||
add_coin(3 * COIN);
|
||||
|
||||
BOOST_CHECK(testWallet.AttemptSelection(1003 * COIN, filter_standard, vCoins, setCoinsRet, nValueRet, coin_selection_params));
|
||||
BOOST_CHECK_EQUAL(nValueRet, 1003 * COIN);
|
||||
BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U);
|
||||
|
||||
empty_wallet();
|
||||
}
|
||||
|
||||
// Tests that with the ideal conditions, the coin selector will always be able to find a solution that can pay the target value
|
||||
BOOST_AUTO_TEST_CASE(SelectCoins_test)
|
||||
{
|
||||
LOCK(testWallet.cs_wallet);
|
||||
testWallet.SetupLegacyScriptPubKeyMan();
|
||||
|
||||
// Random generator stuff
|
||||
std::default_random_engine generator;
|
||||
std::exponential_distribution<double> distribution (100);
|
||||
FastRandomContext rand;
|
||||
|
||||
// Run this test 100 times
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
empty_wallet();
|
||||
|
||||
// Make a wallet with 1000 exponentially distributed random inputs
|
||||
for (int j = 0; j < 1000; ++j)
|
||||
{
|
||||
add_coin((CAmount)(distribution(generator)*10000000));
|
||||
}
|
||||
|
||||
// Generate a random fee rate in the range of 100 - 400
|
||||
CFeeRate rate(rand.randrange(300) + 100);
|
||||
|
||||
// Generate a random target value between 1000 and wallet balance
|
||||
CAmount target = rand.randrange(balance - 1000) + 1000;
|
||||
|
||||
// Perform selection
|
||||
CoinSelectionParams cs_params(/* change_output_size= */ 34,
|
||||
/* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0),
|
||||
/* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
|
||||
/* tx_no_inputs_size= */ 0, /* avoid_partial= */ false);
|
||||
CoinSet out_set;
|
||||
CAmount out_value = 0;
|
||||
CCoinControl cc;
|
||||
BOOST_CHECK(testWallet.SelectCoins(vCoins, target, out_set, out_value, cc, cs_params));
|
||||
BOOST_CHECK_GE(out_value, target);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
83
src/wallet/test/db_tests.cpp
Normal file
83
src/wallet/test/db_tests.cpp
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright (c) 2018-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include <fs.h>
|
||||
#include <test/util/setup_common.h>
|
||||
#include <wallet/bdb.h>
|
||||
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(db_tests, BasicTestingSetup)
|
||||
|
||||
static std::shared_ptr<BerkeleyEnvironment> GetWalletEnv(const fs::path& path, std::string& database_filename)
|
||||
{
|
||||
fs::path data_file = BDBDataFile(path);
|
||||
database_filename = data_file.filename().string();
|
||||
return GetBerkeleyEnv(data_file.parent_path());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(getwalletenv_file)
|
||||
{
|
||||
std::string test_name = "test_name.dat";
|
||||
const fs::path datadir = gArgs.GetDataDirNet();
|
||||
fs::path file_path = datadir / test_name;
|
||||
#if BOOST_VERSION >= 107700
|
||||
std::ofstream f(BOOST_FILESYSTEM_C_STR(file_path));
|
||||
#else
|
||||
std::ofstream f(file_path.BOOST_FILESYSTEM_C_STR);
|
||||
#endif // BOOST_VERSION >= 107700
|
||||
f.close();
|
||||
|
||||
std::string filename;
|
||||
std::shared_ptr<BerkeleyEnvironment> env = GetWalletEnv(file_path, filename);
|
||||
BOOST_CHECK_EQUAL(filename, test_name);
|
||||
BOOST_CHECK_EQUAL(env->Directory(), datadir);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(getwalletenv_directory)
|
||||
{
|
||||
std::string expected_name = "wallet.dat";
|
||||
const fs::path datadir = gArgs.GetDataDirNet();
|
||||
|
||||
std::string filename;
|
||||
std::shared_ptr<BerkeleyEnvironment> env = GetWalletEnv(datadir, filename);
|
||||
BOOST_CHECK_EQUAL(filename, expected_name);
|
||||
BOOST_CHECK_EQUAL(env->Directory(), datadir);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(getwalletenv_g_dbenvs_multiple)
|
||||
{
|
||||
fs::path datadir = gArgs.GetDataDirNet() / "1";
|
||||
fs::path datadir_2 = gArgs.GetDataDirNet() / "2";
|
||||
std::string filename;
|
||||
|
||||
std::shared_ptr<BerkeleyEnvironment> env_1 = GetWalletEnv(datadir, filename);
|
||||
std::shared_ptr<BerkeleyEnvironment> env_2 = GetWalletEnv(datadir, filename);
|
||||
std::shared_ptr<BerkeleyEnvironment> env_3 = GetWalletEnv(datadir_2, filename);
|
||||
|
||||
BOOST_CHECK(env_1 == env_2);
|
||||
BOOST_CHECK(env_2 != env_3);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(getwalletenv_g_dbenvs_free_instance)
|
||||
{
|
||||
fs::path datadir = gArgs.GetDataDirNet() / "1";
|
||||
fs::path datadir_2 = gArgs.GetDataDirNet() / "2";
|
||||
std::string filename;
|
||||
|
||||
std::shared_ptr <BerkeleyEnvironment> env_1_a = GetWalletEnv(datadir, filename);
|
||||
std::shared_ptr <BerkeleyEnvironment> env_2_a = GetWalletEnv(datadir_2, filename);
|
||||
env_1_a.reset();
|
||||
|
||||
std::shared_ptr<BerkeleyEnvironment> env_1_b = GetWalletEnv(datadir, filename);
|
||||
std::shared_ptr<BerkeleyEnvironment> env_2_b = GetWalletEnv(datadir_2, filename);
|
||||
|
||||
BOOST_CHECK(env_1_a != env_1_b);
|
||||
BOOST_CHECK(env_2_a == env_2_b);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
54
src/wallet/test/init_test_fixture.cpp
Normal file
54
src/wallet/test/init_test_fixture.cpp
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) 2018-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <fs.h>
|
||||
#include <univalue.h>
|
||||
#include <util/check.h>
|
||||
#include <util/system.h>
|
||||
|
||||
#include <wallet/test/init_test_fixture.h>
|
||||
|
||||
InitWalletDirTestingSetup::InitWalletDirTestingSetup(const std::string& chainName) : BasicTestingSetup(chainName)
|
||||
{
|
||||
m_wallet_client = MakeWalletClient(*m_node.chain, *Assert(m_node.args));
|
||||
|
||||
std::string sep;
|
||||
sep += fs::path::preferred_separator;
|
||||
|
||||
m_datadir = gArgs.GetDataDirNet();
|
||||
m_cwd = fs::current_path();
|
||||
|
||||
m_walletdir_path_cases["default"] = m_datadir / "wallets";
|
||||
m_walletdir_path_cases["custom"] = m_datadir / "my_wallets";
|
||||
m_walletdir_path_cases["nonexistent"] = m_datadir / "path_does_not_exist";
|
||||
m_walletdir_path_cases["file"] = m_datadir / "not_a_directory.dat";
|
||||
m_walletdir_path_cases["trailing"] = m_datadir / "wallets" / sep;
|
||||
m_walletdir_path_cases["trailing2"] = m_datadir / "wallets" / sep / sep;
|
||||
|
||||
fs::current_path(m_datadir);
|
||||
m_walletdir_path_cases["relative"] = "wallets";
|
||||
|
||||
fs::create_directories(m_walletdir_path_cases["default"]);
|
||||
fs::create_directories(m_walletdir_path_cases["custom"]);
|
||||
fs::create_directories(m_walletdir_path_cases["relative"]);
|
||||
#if BOOST_VERSION >= 107700
|
||||
std::ofstream f(BOOST_FILESYSTEM_C_STR(m_walletdir_path_cases["file"]));
|
||||
#else
|
||||
std::ofstream f(m_walletdir_path_cases["file"].BOOST_FILESYSTEM_C_STR);
|
||||
#endif // BOOST_VERSION >= 107700
|
||||
f.close();
|
||||
}
|
||||
|
||||
InitWalletDirTestingSetup::~InitWalletDirTestingSetup()
|
||||
{
|
||||
gArgs.LockSettings([&](util::Settings& settings) {
|
||||
settings.forced_settings.erase("walletdir");
|
||||
});
|
||||
fs::current_path(m_cwd);
|
||||
}
|
||||
|
||||
void InitWalletDirTestingSetup::SetWalletDir(const fs::path& walletdir_path)
|
||||
{
|
||||
gArgs.ForceSetArg("-walletdir", walletdir_path.string());
|
||||
}
|
||||
25
src/wallet/test/init_test_fixture.h
Normal file
25
src/wallet/test/init_test_fixture.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) 2018-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_TEST_INIT_TEST_FIXTURE_H
|
||||
#define DIGIBYTE_WALLET_TEST_INIT_TEST_FIXTURE_H
|
||||
|
||||
#include <interfaces/chain.h>
|
||||
#include <interfaces/wallet.h>
|
||||
#include <node/context.h>
|
||||
#include <test/util/setup_common.h>
|
||||
|
||||
|
||||
struct InitWalletDirTestingSetup: public BasicTestingSetup {
|
||||
explicit InitWalletDirTestingSetup(const std::string& chainName = CBaseChainParams::MAIN);
|
||||
~InitWalletDirTestingSetup();
|
||||
void SetWalletDir(const fs::path& walletdir_path);
|
||||
|
||||
fs::path m_datadir;
|
||||
fs::path m_cwd;
|
||||
std::map<std::string, fs::path> m_walletdir_path_cases;
|
||||
std::unique_ptr<interfaces::WalletClient> m_wallet_client;
|
||||
};
|
||||
|
||||
#endif // DIGIBYTE_WALLET_TEST_INIT_TEST_FIXTURE_H
|
||||
85
src/wallet/test/init_tests.cpp
Normal file
85
src/wallet/test/init_tests.cpp
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright (c) 2018-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include <noui.h>
|
||||
#include <test/util/logging.h>
|
||||
#include <test/util/setup_common.h>
|
||||
#include <util/system.h>
|
||||
#include <wallet/test/init_test_fixture.h>
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(init_tests, InitWalletDirTestingSetup)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(walletinit_verify_walletdir_default)
|
||||
{
|
||||
SetWalletDir(m_walletdir_path_cases["default"]);
|
||||
bool result = m_wallet_client->verify();
|
||||
BOOST_CHECK(result == true);
|
||||
fs::path walletdir = gArgs.GetArg("-walletdir", "");
|
||||
fs::path expected_path = fs::canonical(m_walletdir_path_cases["default"]);
|
||||
BOOST_CHECK_EQUAL(walletdir, expected_path);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(walletinit_verify_walletdir_custom)
|
||||
{
|
||||
SetWalletDir(m_walletdir_path_cases["custom"]);
|
||||
bool result = m_wallet_client->verify();
|
||||
BOOST_CHECK(result == true);
|
||||
fs::path walletdir = gArgs.GetArg("-walletdir", "");
|
||||
fs::path expected_path = fs::canonical(m_walletdir_path_cases["custom"]);
|
||||
BOOST_CHECK_EQUAL(walletdir, expected_path);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(walletinit_verify_walletdir_does_not_exist)
|
||||
{
|
||||
SetWalletDir(m_walletdir_path_cases["nonexistent"]);
|
||||
{
|
||||
ASSERT_DEBUG_LOG("does not exist");
|
||||
bool result = m_wallet_client->verify();
|
||||
BOOST_CHECK(result == false);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(walletinit_verify_walletdir_is_not_directory)
|
||||
{
|
||||
SetWalletDir(m_walletdir_path_cases["file"]);
|
||||
{
|
||||
ASSERT_DEBUG_LOG("is not a directory");
|
||||
bool result = m_wallet_client->verify();
|
||||
BOOST_CHECK(result == false);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(walletinit_verify_walletdir_is_not_relative)
|
||||
{
|
||||
SetWalletDir(m_walletdir_path_cases["relative"]);
|
||||
{
|
||||
ASSERT_DEBUG_LOG("is a relative path");
|
||||
bool result = m_wallet_client->verify();
|
||||
BOOST_CHECK(result == false);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(walletinit_verify_walletdir_no_trailing)
|
||||
{
|
||||
SetWalletDir(m_walletdir_path_cases["trailing"]);
|
||||
bool result = m_wallet_client->verify();
|
||||
BOOST_CHECK(result == true);
|
||||
fs::path walletdir = gArgs.GetArg("-walletdir", "");
|
||||
fs::path expected_path = fs::canonical(m_walletdir_path_cases["default"]);
|
||||
BOOST_CHECK_EQUAL(walletdir, expected_path);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(walletinit_verify_walletdir_no_trailing2)
|
||||
{
|
||||
SetWalletDir(m_walletdir_path_cases["trailing2"]);
|
||||
bool result = m_wallet_client->verify();
|
||||
BOOST_CHECK(result == true);
|
||||
fs::path walletdir = gArgs.GetArg("-walletdir", "");
|
||||
fs::path expected_path = fs::canonical(m_walletdir_path_cases["default"]);
|
||||
BOOST_CHECK_EQUAL(walletdir, expected_path);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
419
src/wallet/test/ismine_tests.cpp
Normal file
419
src/wallet/test/ismine_tests.cpp
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
// Copyright (c) 2017-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <key.h>
|
||||
#include <node/context.h>
|
||||
#include <script/script.h>
|
||||
#include <script/standard.h>
|
||||
#include <test/util/setup_common.h>
|
||||
#include <wallet/ismine.h>
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(ismine_tests, BasicTestingSetup)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(ismine_standard)
|
||||
{
|
||||
CKey keys[2];
|
||||
CPubKey pubkeys[2];
|
||||
for (int i = 0; i < 2; i++) {
|
||||
keys[i].MakeNewKey(true);
|
||||
pubkeys[i] = keys[i].GetPubKey();
|
||||
}
|
||||
|
||||
CKey uncompressedKey;
|
||||
uncompressedKey.MakeNewKey(false);
|
||||
CPubKey uncompressedPubkey = uncompressedKey.GetPubKey();
|
||||
std::unique_ptr<interfaces::Chain>& chain = m_node.chain;
|
||||
|
||||
CScript scriptPubKey;
|
||||
isminetype result;
|
||||
|
||||
// P2PK compressed
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
scriptPubKey = GetScriptForRawPubKey(pubkeys[0]);
|
||||
|
||||
// Keystore does not have key
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has key
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[0]));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_SPENDABLE);
|
||||
}
|
||||
|
||||
// P2PK uncompressed
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
scriptPubKey = GetScriptForRawPubKey(uncompressedPubkey);
|
||||
|
||||
// Keystore does not have key
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has key
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(uncompressedKey));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_SPENDABLE);
|
||||
}
|
||||
|
||||
// P2PKH compressed
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
scriptPubKey = GetScriptForDestination(PKHash(pubkeys[0]));
|
||||
|
||||
// Keystore does not have key
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has key
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[0]));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_SPENDABLE);
|
||||
}
|
||||
|
||||
// P2PKH uncompressed
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
scriptPubKey = GetScriptForDestination(PKHash(uncompressedPubkey));
|
||||
|
||||
// Keystore does not have key
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has key
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(uncompressedKey));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_SPENDABLE);
|
||||
}
|
||||
|
||||
// P2SH
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
|
||||
CScript redeemScript = GetScriptForDestination(PKHash(pubkeys[0]));
|
||||
scriptPubKey = GetScriptForDestination(ScriptHash(redeemScript));
|
||||
|
||||
// Keystore does not have redeemScript or key
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has redeemScript but no key
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(redeemScript));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has redeemScript and key
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[0]));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_SPENDABLE);
|
||||
}
|
||||
|
||||
// (P2PKH inside) P2SH inside P2SH (invalid)
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
|
||||
CScript redeemscript_inner = GetScriptForDestination(PKHash(pubkeys[0]));
|
||||
CScript redeemscript = GetScriptForDestination(ScriptHash(redeemscript_inner));
|
||||
scriptPubKey = GetScriptForDestination(ScriptHash(redeemscript));
|
||||
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(redeemscript));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(redeemscript_inner));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(scriptPubKey));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[0]));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
}
|
||||
|
||||
// (P2PKH inside) P2SH inside P2WSH (invalid)
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
|
||||
CScript redeemscript = GetScriptForDestination(PKHash(pubkeys[0]));
|
||||
CScript witnessscript = GetScriptForDestination(ScriptHash(redeemscript));
|
||||
scriptPubKey = GetScriptForDestination(WitnessV0ScriptHash(witnessscript));
|
||||
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(witnessscript));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(redeemscript));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(scriptPubKey));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[0]));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
}
|
||||
|
||||
// P2WPKH inside P2WSH (invalid)
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
|
||||
CScript witnessscript = GetScriptForDestination(WitnessV0KeyHash(pubkeys[0]));
|
||||
scriptPubKey = GetScriptForDestination(WitnessV0ScriptHash(witnessscript));
|
||||
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(witnessscript));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(scriptPubKey));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[0]));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
}
|
||||
|
||||
// (P2PKH inside) P2WSH inside P2WSH (invalid)
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
|
||||
CScript witnessscript_inner = GetScriptForDestination(PKHash(pubkeys[0]));
|
||||
CScript witnessscript = GetScriptForDestination(WitnessV0ScriptHash(witnessscript_inner));
|
||||
scriptPubKey = GetScriptForDestination(WitnessV0ScriptHash(witnessscript));
|
||||
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(witnessscript_inner));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(witnessscript));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(scriptPubKey));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[0]));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
}
|
||||
|
||||
// P2WPKH compressed
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[0]));
|
||||
|
||||
scriptPubKey = GetScriptForDestination(WitnessV0KeyHash(pubkeys[0]));
|
||||
|
||||
// Keystore implicitly has key and P2SH redeemScript
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(scriptPubKey));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_SPENDABLE);
|
||||
}
|
||||
|
||||
// P2WPKH uncompressed
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(uncompressedKey));
|
||||
|
||||
scriptPubKey = GetScriptForDestination(WitnessV0KeyHash(uncompressedPubkey));
|
||||
|
||||
// Keystore has key, but no P2SH redeemScript
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has key and P2SH redeemScript
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(scriptPubKey));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
}
|
||||
|
||||
// scriptPubKey multisig
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
|
||||
scriptPubKey = GetScriptForMultisig(2, {uncompressedPubkey, pubkeys[1]});
|
||||
|
||||
// Keystore does not have any keys
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has 1/2 keys
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(uncompressedKey));
|
||||
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has 2/2 keys
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[1]));
|
||||
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has 2/2 keys and the script
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(scriptPubKey));
|
||||
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
}
|
||||
|
||||
// P2SH multisig
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(uncompressedKey));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[1]));
|
||||
|
||||
CScript redeemScript = GetScriptForMultisig(2, {uncompressedPubkey, pubkeys[1]});
|
||||
scriptPubKey = GetScriptForDestination(ScriptHash(redeemScript));
|
||||
|
||||
// Keystore has no redeemScript
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has redeemScript
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(redeemScript));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_SPENDABLE);
|
||||
}
|
||||
|
||||
// P2WSH multisig with compressed keys
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[0]));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[1]));
|
||||
|
||||
CScript witnessScript = GetScriptForMultisig(2, {pubkeys[0], pubkeys[1]});
|
||||
scriptPubKey = GetScriptForDestination(WitnessV0ScriptHash(witnessScript));
|
||||
|
||||
// Keystore has keys, but no witnessScript or P2SH redeemScript
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has keys and witnessScript, but no P2SH redeemScript
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(witnessScript));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has keys, witnessScript, P2SH redeemScript
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(scriptPubKey));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_SPENDABLE);
|
||||
}
|
||||
|
||||
// P2WSH multisig with uncompressed key
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(uncompressedKey));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[1]));
|
||||
|
||||
CScript witnessScript = GetScriptForMultisig(2, {uncompressedPubkey, pubkeys[1]});
|
||||
scriptPubKey = GetScriptForDestination(WitnessV0ScriptHash(witnessScript));
|
||||
|
||||
// Keystore has keys, but no witnessScript or P2SH redeemScript
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has keys and witnessScript, but no P2SH redeemScript
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(witnessScript));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has keys, witnessScript, P2SH redeemScript
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(scriptPubKey));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
}
|
||||
|
||||
// P2WSH multisig wrapped in P2SH
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
|
||||
CScript witnessScript = GetScriptForMultisig(2, {pubkeys[0], pubkeys[1]});
|
||||
CScript redeemScript = GetScriptForDestination(WitnessV0ScriptHash(witnessScript));
|
||||
scriptPubKey = GetScriptForDestination(ScriptHash(redeemScript));
|
||||
|
||||
// Keystore has no witnessScript, P2SH redeemScript, or keys
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has witnessScript and P2SH redeemScript, but no keys
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(redeemScript));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddCScript(witnessScript));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
|
||||
// Keystore has keys, witnessScript, P2SH redeemScript
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[0]));
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[1]));
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_SPENDABLE);
|
||||
}
|
||||
|
||||
// OP_RETURN
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[0]));
|
||||
|
||||
scriptPubKey.clear();
|
||||
scriptPubKey << OP_RETURN << ToByteVector(pubkeys[0]);
|
||||
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
}
|
||||
|
||||
// witness unspendable
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[0]));
|
||||
|
||||
scriptPubKey.clear();
|
||||
scriptPubKey << OP_0 << ToByteVector(ParseHex("aabb"));
|
||||
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
}
|
||||
|
||||
// witness unknown
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[0]));
|
||||
|
||||
scriptPubKey.clear();
|
||||
scriptPubKey << OP_16 << ToByteVector(ParseHex("aabb"));
|
||||
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
}
|
||||
|
||||
// Nonstandard
|
||||
{
|
||||
CWallet keystore(chain.get(), "", CreateDummyWalletDatabase());
|
||||
keystore.SetupLegacyScriptPubKeyMan();
|
||||
LOCK(keystore.GetLegacyScriptPubKeyMan()->cs_KeyStore);
|
||||
BOOST_CHECK(keystore.GetLegacyScriptPubKeyMan()->AddKey(keys[0]));
|
||||
|
||||
scriptPubKey.clear();
|
||||
scriptPubKey << OP_9 << OP_ADD << OP_11 << OP_EQUAL;
|
||||
|
||||
result = keystore.GetLegacyScriptPubKeyMan()->IsMine(scriptPubKey);
|
||||
BOOST_CHECK_EQUAL(result, ISMINE_NO);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
152
src/wallet/test/psbt_wallet_tests.cpp
Normal file
152
src/wallet/test/psbt_wallet_tests.cpp
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright (c) 2017-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2017-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <key_io.h>
|
||||
#include <util/bip32.h>
|
||||
#include <util/strencodings.h>
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
#include <test/util/setup_common.h>
|
||||
#include <wallet/test/wallet_test_fixture.h>
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(psbt_wallet_tests, WalletTestingSetup)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(psbt_updater_test)
|
||||
{
|
||||
auto spk_man = m_wallet.GetOrCreateLegacyScriptPubKeyMan();
|
||||
LOCK2(m_wallet.cs_wallet, spk_man->cs_KeyStore);
|
||||
|
||||
// Create prevtxs and add to wallet
|
||||
CDataStream s_prev_tx1(ParseHex("0200000000010158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88702483045022100a22edcc6e5bc511af4cc4ae0de0fcd75c7e04d8c1c3a8aa9d820ed4b967384ec02200642963597b9b1bc22c75e9f3e117284a962188bf5e8a74c895089046a20ad770121035509a48eb623e10aace8bfd0212fdb8a8e5af3c94b0b133b95e114cab89e4f7965000000"), SER_NETWORK, PROTOCOL_VERSION);
|
||||
CTransactionRef prev_tx1;
|
||||
s_prev_tx1 >> prev_tx1;
|
||||
m_wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(prev_tx1->GetHash()), std::forward_as_tuple(&m_wallet, prev_tx1));
|
||||
|
||||
CDataStream s_prev_tx2(ParseHex("0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000"), SER_NETWORK, PROTOCOL_VERSION);
|
||||
CTransactionRef prev_tx2;
|
||||
s_prev_tx2 >> prev_tx2;
|
||||
m_wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(prev_tx2->GetHash()), std::forward_as_tuple(&m_wallet, prev_tx2));
|
||||
|
||||
// Add scripts
|
||||
CScript rs1;
|
||||
CDataStream s_rs1(ParseHex("475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae"), SER_NETWORK, PROTOCOL_VERSION);
|
||||
s_rs1 >> rs1;
|
||||
spk_man->AddCScript(rs1);
|
||||
|
||||
CScript rs2;
|
||||
CDataStream s_rs2(ParseHex("2200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903"), SER_NETWORK, PROTOCOL_VERSION);
|
||||
s_rs2 >> rs2;
|
||||
spk_man->AddCScript(rs2);
|
||||
|
||||
CScript ws1;
|
||||
CDataStream s_ws1(ParseHex("47522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae"), SER_NETWORK, PROTOCOL_VERSION);
|
||||
s_ws1 >> ws1;
|
||||
spk_man->AddCScript(ws1);
|
||||
|
||||
// Add hd seed
|
||||
CKey key = DecodeSecret("5KSSJQ7UNfFGwVgpCZDSHm5rVNhMFcFtvWM3zQ8mW4qNDEN7LFd"); // Mainnet and uncompressed form of cUkG8i1RFfWGWy5ziR11zJ5V4U4W3viSFCfyJmZnvQaUsd1xuF3T
|
||||
CPubKey master_pub_key = spk_man->DeriveNewSeed(key);
|
||||
spk_man->SetHDSeed(master_pub_key);
|
||||
spk_man->NewKeyPool();
|
||||
|
||||
// Call FillPSBT
|
||||
PartiallySignedTransaction psbtx;
|
||||
CDataStream ssData(ParseHex("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000"), SER_NETWORK, PROTOCOL_VERSION);
|
||||
ssData >> psbtx;
|
||||
|
||||
// Fill transaction with our data
|
||||
bool complete = true;
|
||||
BOOST_REQUIRE_EQUAL(TransactionError::OK, m_wallet.FillPSBT(psbtx, complete, SIGHASH_ALL, false, true));
|
||||
|
||||
// Get the final tx
|
||||
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ssTx << psbtx;
|
||||
std::string final_hex = HexStr(ssTx);
|
||||
BOOST_CHECK_EQUAL(final_hex, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001008a020000000158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8876500000001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88701042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000");
|
||||
|
||||
// Mutate the transaction so that one of the inputs is invalid
|
||||
psbtx.tx->vin[0].prevout.n = 2;
|
||||
|
||||
// Try to sign the mutated input
|
||||
SignatureData sigdata;
|
||||
BOOST_CHECK(spk_man->FillPSBT(psbtx, PrecomputePSBTData(psbtx), SIGHASH_ALL, true, true) != TransactionError::OK);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(parse_hd_keypath)
|
||||
{
|
||||
std::vector<uint32_t> keypath;
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("///////////////////////////", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1'/1", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("//////////////////////////'/", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("1///////////////////////////", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1/1'/", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("1/'//////////////////////////", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath(" ", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("0", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("O", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("0000'/0000'/0000'", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("0000,/0000,/0000,", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("01234", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("0x1234", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("1", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath(" 1", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("42", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("m42", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("4294967295", keypath)); // 4294967295 == 0xFFFFFFFF (uint32_t max)
|
||||
BOOST_CHECK(!ParseHDKeypath("4294967296", keypath)); // 4294967296 == 0xFFFFFFFF (uint32_t max) + 1
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("m", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("n", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("m/", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("n/", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("m/0", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("n/0", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("m/0'", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("m/0''", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("m/0'/0'", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("m/'0/0'", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("m/0/0", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("n/0/0", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("m/0/0/00", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("m/0/0/f00", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("m/0/0/000000000000000000000000000000000000000000000000000000000000000000000000000000000000", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("m/1/1/111111111111111111111111111111111111111111111111111111111111111111111111111111111111", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("m/0/00/0", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("m/0'/00/'0", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("m/1/", keypath));
|
||||
BOOST_CHECK(!ParseHDKeypath("m/1//", keypath));
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("m/0/4294967295", keypath)); // 4294967295 == 0xFFFFFFFF (uint32_t max)
|
||||
BOOST_CHECK(!ParseHDKeypath("m/0/4294967296", keypath)); // 4294967296 == 0xFFFFFFFF (uint32_t max) + 1
|
||||
|
||||
BOOST_CHECK(ParseHDKeypath("m/4294967295", keypath)); // 4294967295 == 0xFFFFFFFF (uint32_t max)
|
||||
BOOST_CHECK(!ParseHDKeypath("m/4294967296", keypath)); // 4294967296 == 0xFFFFFFFF (uint32_t max) + 1
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
41
src/wallet/test/scriptpubkeyman_tests.cpp
Normal file
41
src/wallet/test/scriptpubkeyman_tests.cpp
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) 2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <key.h>
|
||||
#include <script/standard.h>
|
||||
#include <test/util/setup_common.h>
|
||||
#include <wallet/scriptpubkeyman.h>
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(scriptpubkeyman_tests, BasicTestingSetup)
|
||||
|
||||
// Test LegacyScriptPubKeyMan::CanProvide behavior, making sure it returns true
|
||||
// for recognized scripts even when keys may not be available for signing.
|
||||
BOOST_AUTO_TEST_CASE(CanProvide)
|
||||
{
|
||||
// Set up wallet and keyman variables.
|
||||
CWallet wallet(m_node.chain.get(), "", CreateDummyWalletDatabase());
|
||||
LegacyScriptPubKeyMan& keyman = *wallet.GetOrCreateLegacyScriptPubKeyMan();
|
||||
|
||||
// Make a 1 of 2 multisig script
|
||||
std::vector<CKey> keys(2);
|
||||
std::vector<CPubKey> pubkeys;
|
||||
for (CKey& key : keys) {
|
||||
key.MakeNewKey(true);
|
||||
pubkeys.emplace_back(key.GetPubKey());
|
||||
}
|
||||
CScript multisig_script = GetScriptForMultisig(1, pubkeys);
|
||||
CScript p2sh_script = GetScriptForDestination(ScriptHash(multisig_script));
|
||||
SignatureData data;
|
||||
|
||||
// Verify the p2sh(multisig) script is not recognized until the multisig
|
||||
// script is added to the keystore to make it solvable
|
||||
BOOST_CHECK(!keyman.CanProvide(p2sh_script, data));
|
||||
keyman.AddCScript(multisig_script);
|
||||
BOOST_CHECK(keyman.CanProvide(p2sh_script, data));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
127
src/wallet/test/wallet_crypto_tests.cpp
Normal file
127
src/wallet/test/wallet_crypto_tests.cpp
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// Copyright (c) 2009-2019 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2019 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <test/util/setup_common.h>
|
||||
#include <util/strencodings.h>
|
||||
#include <wallet/crypter.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(wallet_crypto_tests, BasicTestingSetup)
|
||||
|
||||
class TestCrypter
|
||||
{
|
||||
public:
|
||||
static void TestPassphraseSingle(const std::vector<unsigned char>& vchSalt, const SecureString& passphrase, uint32_t rounds,
|
||||
const std::vector<unsigned char>& correctKey = std::vector<unsigned char>(),
|
||||
const std::vector<unsigned char>& correctIV=std::vector<unsigned char>())
|
||||
{
|
||||
CCrypter crypt;
|
||||
crypt.SetKeyFromPassphrase(passphrase, vchSalt, rounds, 0);
|
||||
|
||||
if(!correctKey.empty())
|
||||
BOOST_CHECK_MESSAGE(memcmp(crypt.vchKey.data(), correctKey.data(), crypt.vchKey.size()) == 0, \
|
||||
HexStr(crypt.vchKey) + std::string(" != ") + HexStr(correctKey));
|
||||
if(!correctIV.empty())
|
||||
BOOST_CHECK_MESSAGE(memcmp(crypt.vchIV.data(), correctIV.data(), crypt.vchIV.size()) == 0,
|
||||
HexStr(crypt.vchIV) + std::string(" != ") + HexStr(correctIV));
|
||||
}
|
||||
|
||||
static void TestPassphrase(const std::vector<unsigned char>& vchSalt, const SecureString& passphrase, uint32_t rounds,
|
||||
const std::vector<unsigned char>& correctKey = std::vector<unsigned char>(),
|
||||
const std::vector<unsigned char>& correctIV=std::vector<unsigned char>())
|
||||
{
|
||||
TestPassphraseSingle(vchSalt, passphrase, rounds, correctKey, correctIV);
|
||||
for(SecureString::const_iterator i(passphrase.begin()); i != passphrase.end(); ++i)
|
||||
TestPassphraseSingle(vchSalt, SecureString(i, passphrase.end()), rounds);
|
||||
}
|
||||
|
||||
static void TestDecrypt(const CCrypter& crypt, const std::vector<unsigned char>& vchCiphertext, \
|
||||
const std::vector<unsigned char>& vchPlaintext = std::vector<unsigned char>())
|
||||
{
|
||||
CKeyingMaterial vchDecrypted;
|
||||
crypt.Decrypt(vchCiphertext, vchDecrypted);
|
||||
if (vchPlaintext.size())
|
||||
BOOST_CHECK(CKeyingMaterial(vchPlaintext.begin(), vchPlaintext.end()) == vchDecrypted);
|
||||
}
|
||||
|
||||
static void TestEncryptSingle(const CCrypter& crypt, const CKeyingMaterial& vchPlaintext,
|
||||
const std::vector<unsigned char>& vchCiphertextCorrect = std::vector<unsigned char>())
|
||||
{
|
||||
std::vector<unsigned char> vchCiphertext;
|
||||
crypt.Encrypt(vchPlaintext, vchCiphertext);
|
||||
|
||||
if (!vchCiphertextCorrect.empty())
|
||||
BOOST_CHECK(vchCiphertext == vchCiphertextCorrect);
|
||||
|
||||
const std::vector<unsigned char> vchPlaintext2(vchPlaintext.begin(), vchPlaintext.end());
|
||||
TestDecrypt(crypt, vchCiphertext, vchPlaintext2);
|
||||
}
|
||||
|
||||
static void TestEncrypt(const CCrypter& crypt, const std::vector<unsigned char>& vchPlaintextIn, \
|
||||
const std::vector<unsigned char>& vchCiphertextCorrect = std::vector<unsigned char>())
|
||||
{
|
||||
TestEncryptSingle(crypt, CKeyingMaterial(vchPlaintextIn.begin(), vchPlaintextIn.end()), vchCiphertextCorrect);
|
||||
for(std::vector<unsigned char>::const_iterator i(vchPlaintextIn.begin()); i != vchPlaintextIn.end(); ++i)
|
||||
TestEncryptSingle(crypt, CKeyingMaterial(i, vchPlaintextIn.end()));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
BOOST_AUTO_TEST_CASE(passphrase) {
|
||||
// These are expensive.
|
||||
|
||||
TestCrypter::TestPassphrase(ParseHex("0000deadbeef0000"), "test", 25000, \
|
||||
ParseHex("fc7aba077ad5f4c3a0988d8daa4810d0d4a0e3bcb53af662998898f33df0556a"), \
|
||||
ParseHex("cf2f2691526dd1aa220896fb8bf7c369"));
|
||||
|
||||
std::string hash(GetRandHash().ToString());
|
||||
std::vector<unsigned char> vchSalt(8);
|
||||
GetRandBytes(vchSalt.data(), vchSalt.size());
|
||||
uint32_t rounds = InsecureRand32();
|
||||
if (rounds > 30000)
|
||||
rounds = 30000;
|
||||
TestCrypter::TestPassphrase(vchSalt, SecureString(hash.begin(), hash.end()), rounds);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(encrypt) {
|
||||
std::vector<unsigned char> vchSalt = ParseHex("0000deadbeef0000");
|
||||
BOOST_CHECK(vchSalt.size() == WALLET_CRYPTO_SALT_SIZE);
|
||||
CCrypter crypt;
|
||||
crypt.SetKeyFromPassphrase("passphrase", vchSalt, 25000, 0);
|
||||
TestCrypter::TestEncrypt(crypt, ParseHex("22bcade09ac03ff6386914359cfe885cfeb5f77ff0d670f102f619687453b29d"));
|
||||
|
||||
for (int i = 0; i != 100; i++)
|
||||
{
|
||||
uint256 hash(GetRandHash());
|
||||
TestCrypter::TestEncrypt(crypt, std::vector<unsigned char>(hash.begin(), hash.end()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(decrypt) {
|
||||
std::vector<unsigned char> vchSalt = ParseHex("0000deadbeef0000");
|
||||
BOOST_CHECK(vchSalt.size() == WALLET_CRYPTO_SALT_SIZE);
|
||||
CCrypter crypt;
|
||||
crypt.SetKeyFromPassphrase("passphrase", vchSalt, 25000, 0);
|
||||
|
||||
// Some corner cases the came up while testing
|
||||
TestCrypter::TestDecrypt(crypt,ParseHex("795643ce39d736088367822cdc50535ec6f103715e3e48f4f3b1a60a08ef59ca"));
|
||||
TestCrypter::TestDecrypt(crypt,ParseHex("de096f4a8f9bd97db012aa9d90d74de8cdea779c3ee8bc7633d8b5d6da703486"));
|
||||
TestCrypter::TestDecrypt(crypt,ParseHex("32d0a8974e3afd9c6c3ebf4d66aa4e6419f8c173de25947f98cf8b7ace49449c"));
|
||||
TestCrypter::TestDecrypt(crypt,ParseHex("e7c055cca2faa78cb9ac22c9357a90b4778ded9b2cc220a14cea49f931e596ea"));
|
||||
TestCrypter::TestDecrypt(crypt,ParseHex("b88efddd668a6801d19516d6830da4ae9811988ccbaf40df8fbb72f3f4d335fd"));
|
||||
TestCrypter::TestDecrypt(crypt,ParseHex("8cae76aa6a43694e961ebcb28c8ca8f8540b84153d72865e8561ddd93fa7bfa9"));
|
||||
|
||||
for (int i = 0; i != 100; i++)
|
||||
{
|
||||
uint256 hash(GetRandHash());
|
||||
TestCrypter::TestDecrypt(crypt, std::vector<unsigned char>(hash.begin(), hash.end()));
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
15
src/wallet/test/wallet_test_fixture.cpp
Normal file
15
src/wallet/test/wallet_test_fixture.cpp
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright (c) 2009-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <wallet/test/wallet_test_fixture.h>
|
||||
|
||||
WalletTestingSetup::WalletTestingSetup(const std::string& chainName)
|
||||
: TestingSetup(chainName),
|
||||
m_wallet(m_node.chain.get(), "", CreateMockWalletDatabase())
|
||||
{
|
||||
m_wallet.LoadWallet();
|
||||
m_chain_notifications_handler = m_node.chain->handleNotifications({ &m_wallet, [](CWallet*) {} });
|
||||
m_wallet_client->registerRpcs();
|
||||
}
|
||||
29
src/wallet/test/wallet_test_fixture.h
Normal file
29
src/wallet/test/wallet_test_fixture.h
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2016-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2016-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_TEST_WALLET_TEST_FIXTURE_H
|
||||
#define DIGIBYTE_WALLET_TEST_WALLET_TEST_FIXTURE_H
|
||||
|
||||
#include <test/util/setup_common.h>
|
||||
|
||||
#include <interfaces/chain.h>
|
||||
#include <interfaces/wallet.h>
|
||||
#include <node/context.h>
|
||||
#include <util/check.h>
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
/** Testing setup and teardown for wallet.
|
||||
*/
|
||||
struct WalletTestingSetup : public TestingSetup {
|
||||
explicit WalletTestingSetup(const std::string& chainName = CBaseChainParams::MAIN);
|
||||
|
||||
std::unique_ptr<interfaces::WalletClient> m_wallet_client = interfaces::MakeWalletClient(*m_node.chain, *Assert(m_node.args));
|
||||
CWallet m_wallet;
|
||||
std::unique_ptr<interfaces::Handler> m_chain_notifications_handler;
|
||||
};
|
||||
|
||||
#endif // DIGIBYTE_WALLET_TEST_WALLET_TEST_FIXTURE_H
|
||||
830
src/wallet/test/wallet_tests.cpp
Normal file
830
src/wallet/test/wallet_tests.cpp
Normal file
|
|
@ -0,0 +1,830 @@
|
|||
// Copyright (c) 2009-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
#include <any>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <stdint.h>
|
||||
#include <vector>
|
||||
|
||||
#include <chainparams.h>
|
||||
#include <interfaces/chain.h>
|
||||
#include <node/blockstorage.h>
|
||||
#include <node/context.h>
|
||||
#include <policy/policy.h>
|
||||
#include <rpc/server.h>
|
||||
#include <test/util/logging.h>
|
||||
#include <test/util/setup_common.h>
|
||||
#include <util/translation.h>
|
||||
#include <validation.h>
|
||||
#include <wallet/coincontrol.h>
|
||||
#include <wallet/test/wallet_test_fixture.h>
|
||||
#include <policy/policy.h>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
#include <univalue.h>
|
||||
|
||||
RPCHelpMan importmulti();
|
||||
RPCHelpMan dumpwallet();
|
||||
RPCHelpMan importwallet();
|
||||
|
||||
extern RecursiveMutex cs_wallets;
|
||||
|
||||
// Ensure that fee levels defined in the wallet are at least as high
|
||||
// as the default levels for node policy.
|
||||
static_assert(DEFAULT_TRANSACTION_MINFEE >= DEFAULT_MIN_RELAY_TX_FEE, "wallet minimum fee is smaller than default relay fee");
|
||||
static_assert(WALLET_INCREMENTAL_RELAY_FEE >= DEFAULT_INCREMENTAL_RELAY_FEE, "wallet incremental fee is smaller than default incremental relay fee");
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup)
|
||||
|
||||
static std::shared_ptr<CWallet> TestLoadWallet(interfaces::Chain* chain)
|
||||
{
|
||||
DatabaseOptions options;
|
||||
DatabaseStatus status;
|
||||
bilingual_str error;
|
||||
std::vector<bilingual_str> warnings;
|
||||
auto database = MakeWalletDatabase("", options, status, error);
|
||||
auto wallet = CWallet::Create(chain, "", std::move(database), options.create_flags, error, warnings);
|
||||
if (chain) {
|
||||
wallet->postInitProcess();
|
||||
}
|
||||
return wallet;
|
||||
}
|
||||
|
||||
static void TestUnloadWallet(std::shared_ptr<CWallet>&& wallet)
|
||||
{
|
||||
SyncWithValidationInterfaceQueue();
|
||||
wallet->m_chain_notifications_handler.reset();
|
||||
UnloadWallet(std::move(wallet));
|
||||
}
|
||||
|
||||
static CMutableTransaction TestSimpleSpend(const CTransaction& from, uint32_t index, const CKey& key, const CScript& pubkey)
|
||||
{
|
||||
CMutableTransaction mtx;
|
||||
mtx.vout.push_back({from.vout[index].nValue - DEFAULT_TRANSACTION_MAXFEE, pubkey});
|
||||
mtx.vin.push_back({CTxIn{from.GetHash(), index}});
|
||||
FillableSigningProvider keystore;
|
||||
keystore.AddKey(key);
|
||||
std::map<COutPoint, Coin> coins;
|
||||
coins[mtx.vin[0].prevout].out = from.vout[index];
|
||||
std::map<int, std::string> input_errors;
|
||||
BOOST_CHECK(SignTransaction(mtx, &keystore, coins, SIGHASH_ALL, input_errors));
|
||||
return mtx;
|
||||
}
|
||||
|
||||
static void AddKey(CWallet& wallet, const CKey& key)
|
||||
{
|
||||
auto spk_man = wallet.GetOrCreateLegacyScriptPubKeyMan();
|
||||
LOCK2(wallet.cs_wallet, spk_man->cs_KeyStore);
|
||||
spk_man->AddKeyPubKey(key, key.GetPubKey());
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
|
||||
{
|
||||
// Cap last block file size, and mine new block in a new block file.
|
||||
CBlockIndex* oldTip = m_node.chainman->ActiveChain().Tip();
|
||||
GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE;
|
||||
CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
|
||||
CBlockIndex* newTip = m_node.chainman->ActiveChain().Tip();
|
||||
|
||||
// Verify ScanForWalletTransactions fails to read an unknown start block.
|
||||
{
|
||||
CWallet wallet(m_node.chain.get(), "", CreateDummyWalletDatabase());
|
||||
{
|
||||
LOCK(wallet.cs_wallet);
|
||||
wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
||||
}
|
||||
AddKey(wallet, coinbaseKey);
|
||||
WalletRescanReserver reserver(wallet);
|
||||
reserver.reserve();
|
||||
CWallet::ScanResult result = wallet.ScanForWalletTransactions({} /* start_block */, 0 /* start_height */, {} /* max_height */, reserver, false /* update */);
|
||||
BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
|
||||
BOOST_CHECK(result.last_failed_block.IsNull());
|
||||
BOOST_CHECK(result.last_scanned_block.IsNull());
|
||||
BOOST_CHECK(!result.last_scanned_height);
|
||||
BOOST_CHECK_EQUAL(wallet.GetBalance().m_mine_immature, 0);
|
||||
}
|
||||
|
||||
// Verify ScanForWalletTransactions picks up transactions in both the old
|
||||
// and new block files.
|
||||
{
|
||||
CWallet wallet(m_node.chain.get(), "", CreateDummyWalletDatabase());
|
||||
{
|
||||
LOCK(wallet.cs_wallet);
|
||||
wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
||||
}
|
||||
AddKey(wallet, coinbaseKey);
|
||||
WalletRescanReserver reserver(wallet);
|
||||
reserver.reserve();
|
||||
CWallet::ScanResult result = wallet.ScanForWalletTransactions(oldTip->GetBlockHash(), oldTip->nHeight, {} /* max_height */, reserver, false /* update */);
|
||||
BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
|
||||
BOOST_CHECK(result.last_failed_block.IsNull());
|
||||
BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash());
|
||||
BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight);
|
||||
BOOST_CHECK_EQUAL(wallet.GetBalance().m_mine_immature, 2 * 72000 * COIN);
|
||||
}
|
||||
|
||||
// Prune the older block file.
|
||||
{
|
||||
LOCK(cs_main);
|
||||
Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(oldTip->GetBlockPos().nFile);
|
||||
}
|
||||
UnlinkPrunedFiles({oldTip->GetBlockPos().nFile});
|
||||
|
||||
// Verify ScanForWalletTransactions only picks transactions in the new block
|
||||
// file.
|
||||
{
|
||||
CWallet wallet(m_node.chain.get(), "", CreateDummyWalletDatabase());
|
||||
{
|
||||
LOCK(wallet.cs_wallet);
|
||||
wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
||||
}
|
||||
AddKey(wallet, coinbaseKey);
|
||||
WalletRescanReserver reserver(wallet);
|
||||
reserver.reserve();
|
||||
CWallet::ScanResult result = wallet.ScanForWalletTransactions(oldTip->GetBlockHash(), oldTip->nHeight, {} /* max_height */, reserver, false /* update */);
|
||||
BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
|
||||
BOOST_CHECK_EQUAL(result.last_failed_block, oldTip->GetBlockHash());
|
||||
BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash());
|
||||
BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight);
|
||||
BOOST_CHECK_EQUAL(wallet.GetBalance().m_mine_immature, 72000 * COIN);
|
||||
}
|
||||
|
||||
// Prune the remaining block file.
|
||||
{
|
||||
LOCK(cs_main);
|
||||
Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(newTip->GetBlockPos().nFile);
|
||||
}
|
||||
UnlinkPrunedFiles({newTip->GetBlockPos().nFile});
|
||||
|
||||
// Verify ScanForWalletTransactions scans no blocks.
|
||||
{
|
||||
CWallet wallet(m_node.chain.get(), "", CreateDummyWalletDatabase());
|
||||
{
|
||||
LOCK(wallet.cs_wallet);
|
||||
wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
||||
}
|
||||
AddKey(wallet, coinbaseKey);
|
||||
WalletRescanReserver reserver(wallet);
|
||||
reserver.reserve();
|
||||
CWallet::ScanResult result = wallet.ScanForWalletTransactions(oldTip->GetBlockHash(), oldTip->nHeight, {} /* max_height */, reserver, false /* update */);
|
||||
BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
|
||||
BOOST_CHECK_EQUAL(result.last_failed_block, newTip->GetBlockHash());
|
||||
BOOST_CHECK(result.last_scanned_block.IsNull());
|
||||
BOOST_CHECK(!result.last_scanned_height);
|
||||
BOOST_CHECK_EQUAL(wallet.GetBalance().m_mine_immature, 0);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup)
|
||||
{
|
||||
// Cap last block file size, and mine new block in a new block file.
|
||||
CBlockIndex* oldTip = m_node.chainman->ActiveChain().Tip();
|
||||
GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE;
|
||||
CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
|
||||
CBlockIndex* newTip = m_node.chainman->ActiveChain().Tip();
|
||||
|
||||
// Prune the older block file.
|
||||
{
|
||||
LOCK(cs_main);
|
||||
Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(oldTip->GetBlockPos().nFile);
|
||||
}
|
||||
UnlinkPrunedFiles({oldTip->GetBlockPos().nFile});
|
||||
|
||||
// Verify importmulti RPC returns failure for a key whose creation time is
|
||||
// before the missing block, and success for a key whose creation time is
|
||||
// after.
|
||||
{
|
||||
std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateDummyWalletDatabase());
|
||||
wallet->SetupLegacyScriptPubKeyMan();
|
||||
WITH_LOCK(wallet->cs_wallet, wallet->SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash()));
|
||||
AddWallet(wallet);
|
||||
UniValue keys;
|
||||
keys.setArray();
|
||||
UniValue key;
|
||||
key.setObject();
|
||||
key.pushKV("scriptPubKey", HexStr(GetScriptForRawPubKey(coinbaseKey.GetPubKey())));
|
||||
key.pushKV("timestamp", 0);
|
||||
key.pushKV("internal", UniValue(true));
|
||||
keys.push_back(key);
|
||||
key.clear();
|
||||
key.setObject();
|
||||
CKey futureKey;
|
||||
futureKey.MakeNewKey(true);
|
||||
key.pushKV("scriptPubKey", HexStr(GetScriptForRawPubKey(futureKey.GetPubKey())));
|
||||
key.pushKV("timestamp", newTip->GetBlockTimeMax() + TIMESTAMP_WINDOW + 1);
|
||||
key.pushKV("internal", UniValue(true));
|
||||
keys.push_back(key);
|
||||
JSONRPCRequest request;
|
||||
request.params.setArray();
|
||||
request.params.push_back(keys);
|
||||
|
||||
UniValue response = importmulti().HandleRequest(request);
|
||||
BOOST_CHECK_EQUAL(response.write(),
|
||||
strprintf("[{\"success\":false,\"error\":{\"code\":-1,\"message\":\"Rescan failed for key with creation "
|
||||
"timestamp %d. There was an error reading a block from time %d, which is after or within %d "
|
||||
"seconds of key creation, and could contain transactions pertaining to the key. As a result, "
|
||||
"transactions and coins using this key may not appear in the wallet. This error could be caused "
|
||||
"by pruning or data corruption (see vextad log for details) and could be dealt with by "
|
||||
"downloading and rescanning the relevant blocks (see -reindex and -rescan "
|
||||
"options).\"}},{\"success\":true}]",
|
||||
0, oldTip->GetBlockTimeMax(), TIMESTAMP_WINDOW));
|
||||
RemoveWallet(wallet, std::nullopt);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify importwallet RPC starts rescan at earliest block with timestamp
|
||||
// greater or equal than key birthday. Previously there was a bug where
|
||||
// importwallet RPC would start the scan at the latest block with timestamp less
|
||||
// than or equal to key birthday.
|
||||
BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup)
|
||||
{
|
||||
// Create two blocks with same timestamp to verify that importwallet rescan
|
||||
// will pick up both blocks, not just the first.
|
||||
const int64_t BLOCK_TIME = m_node.chainman->ActiveChain().Tip()->GetBlockTimeMax() + 5;
|
||||
SetMockTime(BLOCK_TIME);
|
||||
m_coinbase_txns.emplace_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
||||
m_coinbase_txns.emplace_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
||||
|
||||
// Set key birthday to block time increased by the timestamp window, so
|
||||
// rescan will start at the block time.
|
||||
const int64_t KEY_TIME = BLOCK_TIME + TIMESTAMP_WINDOW;
|
||||
SetMockTime(KEY_TIME);
|
||||
m_coinbase_txns.emplace_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
||||
|
||||
std::string backup_file = (gArgs.GetDataDirNet() / "wallet.backup").string();
|
||||
|
||||
// Import key into wallet and call dumpwallet to create backup file.
|
||||
{
|
||||
std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateDummyWalletDatabase());
|
||||
{
|
||||
auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan();
|
||||
LOCK2(wallet->cs_wallet, spk_man->cs_KeyStore);
|
||||
spk_man->mapKeyMetadata[coinbaseKey.GetPubKey().GetID()].nCreateTime = KEY_TIME;
|
||||
spk_man->AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey());
|
||||
|
||||
AddWallet(wallet);
|
||||
wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
||||
}
|
||||
JSONRPCRequest request;
|
||||
request.params.setArray();
|
||||
request.params.push_back(backup_file);
|
||||
|
||||
::dumpwallet().HandleRequest(request);
|
||||
RemoveWallet(wallet, std::nullopt);
|
||||
}
|
||||
|
||||
// Call importwallet RPC and verify all blocks with timestamps >= BLOCK_TIME
|
||||
// were scanned, and no prior blocks were scanned.
|
||||
{
|
||||
std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateDummyWalletDatabase());
|
||||
LOCK(wallet->cs_wallet);
|
||||
wallet->SetupLegacyScriptPubKeyMan();
|
||||
|
||||
JSONRPCRequest request;
|
||||
request.params.setArray();
|
||||
request.params.push_back(backup_file);
|
||||
AddWallet(wallet);
|
||||
wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
||||
::importwallet().HandleRequest(request);
|
||||
RemoveWallet(wallet, std::nullopt);
|
||||
|
||||
BOOST_CHECK_EQUAL(wallet->mapWallet.size(), 3U);
|
||||
BOOST_CHECK_EQUAL(m_coinbase_txns.size(), 103U);
|
||||
for (size_t i = 0; i < m_coinbase_txns.size(); ++i) {
|
||||
bool found = wallet->GetWalletTx(m_coinbase_txns[i]->GetHash());
|
||||
bool expected = i >= 100;
|
||||
BOOST_CHECK_EQUAL(found, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check that GetImmatureCredit() returns a newly calculated value instead of
|
||||
// the cached value after a MarkDirty() call.
|
||||
//
|
||||
// This is a regression test written to verify a bugfix for the immature credit
|
||||
// function. Similar tests probably should be written for the other credit and
|
||||
// debit functions.
|
||||
BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup)
|
||||
{
|
||||
CWallet wallet(m_node.chain.get(), "", CreateDummyWalletDatabase());
|
||||
auto spk_man = wallet.GetOrCreateLegacyScriptPubKeyMan();
|
||||
CWalletTx wtx(&wallet, m_coinbase_txns.back());
|
||||
|
||||
LOCK2(wallet.cs_wallet, spk_man->cs_KeyStore);
|
||||
wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
||||
|
||||
CWalletTx::Confirmation confirm(CWalletTx::Status::CONFIRMED, m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash(), 0);
|
||||
wtx.m_confirm = confirm;
|
||||
|
||||
// Call GetImmatureCredit() once before adding the key to the wallet to
|
||||
// cache the current immature credit amount, which is 0.
|
||||
BOOST_CHECK_EQUAL(wtx.GetImmatureCredit(), 0);
|
||||
|
||||
// Invalidate the cached value, add the key, and make sure a new immature
|
||||
// credit amount is calculated.
|
||||
wtx.MarkDirty();
|
||||
BOOST_CHECK(spk_man->AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey()));
|
||||
BOOST_CHECK_EQUAL(wtx.GetImmatureCredit(), 72000*COIN);
|
||||
}
|
||||
|
||||
static int64_t AddTx(ChainstateManager& chainman, CWallet& wallet, uint32_t lockTime, int64_t mockTime, int64_t blockTime)
|
||||
{
|
||||
CMutableTransaction tx;
|
||||
CWalletTx::Confirmation confirm;
|
||||
tx.nLockTime = lockTime;
|
||||
SetMockTime(mockTime);
|
||||
CBlockIndex* block = nullptr;
|
||||
if (blockTime > 0) {
|
||||
LOCK(cs_main);
|
||||
auto inserted = chainman.BlockIndex().emplace(GetRandHash(), new CBlockIndex);
|
||||
assert(inserted.second);
|
||||
const uint256& hash = inserted.first->first;
|
||||
block = inserted.first->second;
|
||||
block->nTime = blockTime;
|
||||
block->phashBlock = &hash;
|
||||
confirm = {CWalletTx::Status::CONFIRMED, block->nHeight, hash, 0};
|
||||
}
|
||||
|
||||
// If transaction is already in map, to avoid inconsistencies, unconfirmation
|
||||
// is needed before confirm again with different block.
|
||||
return wallet.AddToWallet(MakeTransactionRef(tx), confirm, [&](CWalletTx& wtx, bool /* new_tx */) {
|
||||
wtx.setUnconfirmed();
|
||||
return true;
|
||||
})->nTimeSmart;
|
||||
}
|
||||
|
||||
// Simple test to verify assignment of CWalletTx::nSmartTime value. Could be
|
||||
// expanded to cover more corner cases of smart time logic.
|
||||
BOOST_AUTO_TEST_CASE(ComputeTimeSmart)
|
||||
{
|
||||
// New transaction should use clock time if lower than block time.
|
||||
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 100, 120), 100);
|
||||
|
||||
// Test that updating existing transaction does not change smart time.
|
||||
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 200, 220), 100);
|
||||
|
||||
// New transaction should use clock time if there's no block time.
|
||||
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 2, 300, 0), 300);
|
||||
|
||||
// New transaction should use block time if lower than clock time.
|
||||
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 3, 420, 400), 400);
|
||||
|
||||
// New transaction should use latest entry time if higher than
|
||||
// min(block time, clock time).
|
||||
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 4, 500, 390), 400);
|
||||
|
||||
// If there are future entries, new transaction should use time of the
|
||||
// newest entry that is no more than 300 seconds ahead of the clock time.
|
||||
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 5, 50, 600), 300);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(LoadReceiveRequests)
|
||||
{
|
||||
CTxDestination dest = PKHash();
|
||||
LOCK(m_wallet.cs_wallet);
|
||||
WalletBatch batch{m_wallet.GetDatabase()};
|
||||
m_wallet.SetAddressUsed(batch, dest, true);
|
||||
m_wallet.SetAddressReceiveRequest(batch, dest, "0", "val_rr0");
|
||||
m_wallet.SetAddressReceiveRequest(batch, dest, "1", "val_rr1");
|
||||
|
||||
auto values = m_wallet.GetAddressReceiveRequests();
|
||||
BOOST_CHECK_EQUAL(values.size(), 2U);
|
||||
BOOST_CHECK_EQUAL(values[0], "val_rr0");
|
||||
BOOST_CHECK_EQUAL(values[1], "val_rr1");
|
||||
}
|
||||
|
||||
// Test some watch-only LegacyScriptPubKeyMan methods by the procedure of loading (LoadWatchOnly),
|
||||
// checking (HaveWatchOnly), getting (GetWatchPubKey) and removing (RemoveWatchOnly) a
|
||||
// given PubKey, resp. its corresponding P2PK Script. Results of the the impact on
|
||||
// the address -> PubKey map is dependent on whether the PubKey is a point on the curve
|
||||
static void TestWatchOnlyPubKey(LegacyScriptPubKeyMan* spk_man, const CPubKey& add_pubkey)
|
||||
{
|
||||
CScript p2pk = GetScriptForRawPubKey(add_pubkey);
|
||||
CKeyID add_address = add_pubkey.GetID();
|
||||
CPubKey found_pubkey;
|
||||
LOCK(spk_man->cs_KeyStore);
|
||||
|
||||
// all Scripts (i.e. also all PubKeys) are added to the general watch-only set
|
||||
BOOST_CHECK(!spk_man->HaveWatchOnly(p2pk));
|
||||
spk_man->LoadWatchOnly(p2pk);
|
||||
BOOST_CHECK(spk_man->HaveWatchOnly(p2pk));
|
||||
|
||||
// only PubKeys on the curve shall be added to the watch-only address -> PubKey map
|
||||
bool is_pubkey_fully_valid = add_pubkey.IsFullyValid();
|
||||
if (is_pubkey_fully_valid) {
|
||||
BOOST_CHECK(spk_man->GetWatchPubKey(add_address, found_pubkey));
|
||||
BOOST_CHECK(found_pubkey == add_pubkey);
|
||||
} else {
|
||||
BOOST_CHECK(!spk_man->GetWatchPubKey(add_address, found_pubkey));
|
||||
BOOST_CHECK(found_pubkey == CPubKey()); // passed key is unchanged
|
||||
}
|
||||
|
||||
spk_man->RemoveWatchOnly(p2pk);
|
||||
BOOST_CHECK(!spk_man->HaveWatchOnly(p2pk));
|
||||
|
||||
if (is_pubkey_fully_valid) {
|
||||
BOOST_CHECK(!spk_man->GetWatchPubKey(add_address, found_pubkey));
|
||||
BOOST_CHECK(found_pubkey == add_pubkey); // passed key is unchanged
|
||||
}
|
||||
}
|
||||
|
||||
// Cryptographically invalidate a PubKey whilst keeping length and first byte
|
||||
static void PollutePubKey(CPubKey& pubkey)
|
||||
{
|
||||
std::vector<unsigned char> pubkey_raw(pubkey.begin(), pubkey.end());
|
||||
std::fill(pubkey_raw.begin()+1, pubkey_raw.end(), 0);
|
||||
pubkey = CPubKey(pubkey_raw);
|
||||
assert(!pubkey.IsFullyValid());
|
||||
assert(pubkey.IsValid());
|
||||
}
|
||||
|
||||
// Test watch-only logic for PubKeys
|
||||
BOOST_AUTO_TEST_CASE(WatchOnlyPubKeys)
|
||||
{
|
||||
CKey key;
|
||||
CPubKey pubkey;
|
||||
LegacyScriptPubKeyMan* spk_man = m_wallet.GetOrCreateLegacyScriptPubKeyMan();
|
||||
|
||||
BOOST_CHECK(!spk_man->HaveWatchOnly());
|
||||
|
||||
// uncompressed valid PubKey
|
||||
key.MakeNewKey(false);
|
||||
pubkey = key.GetPubKey();
|
||||
assert(!pubkey.IsCompressed());
|
||||
TestWatchOnlyPubKey(spk_man, pubkey);
|
||||
|
||||
// uncompressed cryptographically invalid PubKey
|
||||
PollutePubKey(pubkey);
|
||||
TestWatchOnlyPubKey(spk_man, pubkey);
|
||||
|
||||
// compressed valid PubKey
|
||||
key.MakeNewKey(true);
|
||||
pubkey = key.GetPubKey();
|
||||
assert(pubkey.IsCompressed());
|
||||
TestWatchOnlyPubKey(spk_man, pubkey);
|
||||
|
||||
// compressed cryptographically invalid PubKey
|
||||
PollutePubKey(pubkey);
|
||||
TestWatchOnlyPubKey(spk_man, pubkey);
|
||||
|
||||
// invalid empty PubKey
|
||||
pubkey = CPubKey();
|
||||
TestWatchOnlyPubKey(spk_man, pubkey);
|
||||
}
|
||||
|
||||
class ListCoinsTestingSetup : public TestChain100Setup
|
||||
{
|
||||
public:
|
||||
ListCoinsTestingSetup()
|
||||
{
|
||||
CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
|
||||
wallet = std::make_unique<CWallet>(m_node.chain.get(), "", CreateMockWalletDatabase());
|
||||
{
|
||||
LOCK2(wallet->cs_wallet, ::cs_main);
|
||||
wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
||||
}
|
||||
wallet->LoadWallet();
|
||||
AddKey(*wallet, coinbaseKey);
|
||||
WalletRescanReserver reserver(*wallet);
|
||||
reserver.reserve();
|
||||
CWallet::ScanResult result = wallet->ScanForWalletTransactions(m_node.chainman->ActiveChain().Genesis()->GetBlockHash(), 0 /* start_height */, {} /* max_height */, reserver, false /* update */);
|
||||
BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
|
||||
BOOST_CHECK_EQUAL(result.last_scanned_block, m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
||||
BOOST_CHECK_EQUAL(*result.last_scanned_height, m_node.chainman->ActiveChain().Height());
|
||||
BOOST_CHECK(result.last_failed_block.IsNull());
|
||||
}
|
||||
|
||||
~ListCoinsTestingSetup()
|
||||
{
|
||||
wallet.reset();
|
||||
}
|
||||
|
||||
CWalletTx& AddTx(CRecipient recipient)
|
||||
{
|
||||
CTransactionRef tx;
|
||||
CAmount fee;
|
||||
int changePos = -1;
|
||||
bilingual_str error;
|
||||
CCoinControl dummy;
|
||||
FeeCalculation fee_calc_out;
|
||||
{
|
||||
BOOST_CHECK(wallet->CreateTransaction({recipient}, tx, fee, changePos, error, dummy, fee_calc_out));
|
||||
}
|
||||
wallet->CommitTransaction(tx, {}, {});
|
||||
CMutableTransaction blocktx;
|
||||
{
|
||||
LOCK(wallet->cs_wallet);
|
||||
blocktx = CMutableTransaction(*wallet->mapWallet.at(tx->GetHash()).tx);
|
||||
}
|
||||
CreateAndProcessBlock({CMutableTransaction(blocktx)}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
|
||||
|
||||
LOCK(wallet->cs_wallet);
|
||||
wallet->SetLastBlockProcessed(wallet->GetLastBlockHeight() + 1, m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
||||
auto it = wallet->mapWallet.find(tx->GetHash());
|
||||
BOOST_CHECK(it != wallet->mapWallet.end());
|
||||
CWalletTx::Confirmation confirm(CWalletTx::Status::CONFIRMED, m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash(), 1);
|
||||
it->second.m_confirm = confirm;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::unique_ptr<CWallet> wallet;
|
||||
};
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(ListCoins, ListCoinsTestingSetup)
|
||||
{
|
||||
std::string coinbaseAddress = coinbaseKey.GetPubKey().GetID().ToString();
|
||||
|
||||
// Confirm ListCoins initially returns 1 coin grouped under coinbaseKey
|
||||
// address.
|
||||
std::map<CTxDestination, std::vector<COutput>> list;
|
||||
{
|
||||
LOCK(wallet->cs_wallet);
|
||||
list = wallet->ListCoins();
|
||||
}
|
||||
BOOST_CHECK_EQUAL(list.size(), 1U);
|
||||
BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress);
|
||||
BOOST_CHECK_EQUAL(list.begin()->second.size(), 1U);
|
||||
|
||||
// Check initial balance from one mature coinbase transaction.
|
||||
BOOST_CHECK_EQUAL(72000 * COIN, wallet->GetAvailableBalance());
|
||||
|
||||
// Add a transaction creating a change address, and confirm ListCoins still
|
||||
// returns the coin associated with the change address underneath the
|
||||
// coinbaseKey pubkey, even though the change address has a different
|
||||
// pubkey.
|
||||
AddTx(CRecipient{GetScriptForRawPubKey({}), 1 * COIN, false /* subtract fee */});
|
||||
{
|
||||
LOCK(wallet->cs_wallet);
|
||||
list = wallet->ListCoins();
|
||||
}
|
||||
BOOST_CHECK_EQUAL(list.size(), 1U);
|
||||
BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress);
|
||||
BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U);
|
||||
|
||||
// Lock both coins. Confirm number of available coins drops to 0.
|
||||
{
|
||||
LOCK(wallet->cs_wallet);
|
||||
std::vector<COutput> available;
|
||||
wallet->AvailableCoins(available);
|
||||
BOOST_CHECK_EQUAL(available.size(), 2U);
|
||||
}
|
||||
for (const auto& group : list) {
|
||||
for (const auto& coin : group.second) {
|
||||
LOCK(wallet->cs_wallet);
|
||||
wallet->LockCoin(COutPoint(coin.tx->GetHash(), coin.i));
|
||||
}
|
||||
}
|
||||
{
|
||||
LOCK(wallet->cs_wallet);
|
||||
std::vector<COutput> available;
|
||||
wallet->AvailableCoins(available);
|
||||
BOOST_CHECK_EQUAL(available.size(), 0U);
|
||||
}
|
||||
// Confirm ListCoins still returns same result as before, despite coins
|
||||
// being locked.
|
||||
{
|
||||
LOCK(wallet->cs_wallet);
|
||||
list = wallet->ListCoins();
|
||||
}
|
||||
BOOST_CHECK_EQUAL(list.size(), 1U);
|
||||
BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress);
|
||||
BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U);
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup)
|
||||
{
|
||||
std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateDummyWalletDatabase());
|
||||
wallet->SetupLegacyScriptPubKeyMan();
|
||||
wallet->SetMinVersion(FEATURE_LATEST);
|
||||
wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
|
||||
BOOST_CHECK(!wallet->TopUpKeyPool(1000));
|
||||
CTxDestination dest;
|
||||
std::string error;
|
||||
BOOST_CHECK(!wallet->GetNewDestination(OutputType::BECH32, "", dest, error));
|
||||
}
|
||||
|
||||
// Explicit calculation which is used to test the wallet constant
|
||||
// We get the same virtual size due to rounding(weight/4) for both use_max_sig values
|
||||
static size_t CalculateNestedKeyhashInputSize(bool use_max_sig)
|
||||
{
|
||||
// Generate ephemeral valid pubkey
|
||||
CKey key;
|
||||
key.MakeNewKey(true);
|
||||
CPubKey pubkey = key.GetPubKey();
|
||||
|
||||
// Generate pubkey hash
|
||||
uint160 key_hash(Hash160(pubkey));
|
||||
|
||||
// Create inner-script to enter into keystore. Key hash can't be 0...
|
||||
CScript inner_script = CScript() << OP_0 << std::vector<unsigned char>(key_hash.begin(), key_hash.end());
|
||||
|
||||
// Create outer P2SH script for the output
|
||||
uint160 script_id(Hash160(inner_script));
|
||||
CScript script_pubkey = CScript() << OP_HASH160 << std::vector<unsigned char>(script_id.begin(), script_id.end()) << OP_EQUAL;
|
||||
|
||||
// Add inner-script to key store and key to watchonly
|
||||
FillableSigningProvider keystore;
|
||||
keystore.AddCScript(inner_script);
|
||||
keystore.AddKeyPubKey(key, pubkey);
|
||||
|
||||
// Fill in dummy signatures for fee calculation.
|
||||
SignatureData sig_data;
|
||||
|
||||
if (!ProduceSignature(keystore, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, script_pubkey, sig_data)) {
|
||||
// We're hand-feeding it correct arguments; shouldn't happen
|
||||
assert(false);
|
||||
}
|
||||
|
||||
CTxIn tx_in;
|
||||
UpdateInput(tx_in, sig_data);
|
||||
return (size_t)GetVirtualTransactionInputSize(tx_in);
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(dummy_input_size_test, TestChain100Setup)
|
||||
{
|
||||
BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(false), DUMMY_NESTED_P2WPKH_INPUT_SIZE);
|
||||
BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(true), DUMMY_NESTED_P2WPKH_INPUT_SIZE);
|
||||
}
|
||||
|
||||
bool malformed_descriptor(std::ios_base::failure e)
|
||||
{
|
||||
std::string s(e.what());
|
||||
return s.find("Missing checksum") != std::string::npos;
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup)
|
||||
{
|
||||
std::vector<unsigned char> malformed_record;
|
||||
CVectorWriter vw(0, 0, malformed_record, 0);
|
||||
vw << std::string("notadescriptor");
|
||||
vw << (uint64_t)0;
|
||||
vw << (int32_t)0;
|
||||
vw << (int32_t)0;
|
||||
vw << (int32_t)1;
|
||||
|
||||
VectorReader vr(0, 0, malformed_record, 0);
|
||||
WalletDescriptor w_desc;
|
||||
BOOST_CHECK_EXCEPTION(vr >> w_desc, std::ios_base::failure, malformed_descriptor);
|
||||
}
|
||||
|
||||
//! Test CWallet::Create() and its behavior handling potential race
|
||||
//! conditions if it's called the same time an incoming transaction shows up in
|
||||
//! the mempool or a new block.
|
||||
//!
|
||||
//! It isn't possible to verify there aren't race condition in every case, so
|
||||
//! this test just checks two specific cases and ensures that timing of
|
||||
//! notifications in these cases doesn't prevent the wallet from detecting
|
||||
//! transactions.
|
||||
//!
|
||||
//! In the first case, block and mempool transactions are created before the
|
||||
//! wallet is loaded, but notifications about these transactions are delayed
|
||||
//! until after it is loaded. The notifications are superfluous in this case, so
|
||||
//! the test verifies the transactions are detected before they arrive.
|
||||
//!
|
||||
//! In the second case, block and mempool transactions are created after the
|
||||
//! wallet rescan and notifications are immediately synced, to verify the wallet
|
||||
//! must already have a handler in place for them, and there's no gap after
|
||||
//! rescanning where new transactions in new blocks could be lost.
|
||||
BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup)
|
||||
{
|
||||
gArgs.ForceSetArg("-unsafesqlitesync", "1");
|
||||
gArgs.ForceSetArg("-dandelion", "0");
|
||||
// Create new wallet with known key and unload it.
|
||||
auto wallet = TestLoadWallet(m_node.chain.get());
|
||||
CKey key;
|
||||
key.MakeNewKey(true);
|
||||
AddKey(*wallet, key);
|
||||
TestUnloadWallet(std::move(wallet));
|
||||
|
||||
|
||||
// Add log hook to detect AddToWallet events from rescans, blockConnected,
|
||||
// and transactionAddedToMempool notifications
|
||||
int addtx_count = 0;
|
||||
DebugLogHelper addtx_counter("[default wallet] AddToWallet", [&](const std::string* s) {
|
||||
if (s) ++addtx_count;
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
bool rescan_completed = false;
|
||||
DebugLogHelper rescan_check("[default wallet] Rescan completed", [&](const std::string* s) {
|
||||
if (s) rescan_completed = true;
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
// Block the queue to prevent the wallet receiving blockConnected and
|
||||
// transactionAddedToMempool notifications, and create block and mempool
|
||||
// transactions paying to the wallet
|
||||
std::promise<void> promise;
|
||||
CallFunctionInValidationInterfaceQueue([&promise] {
|
||||
promise.get_future().wait();
|
||||
});
|
||||
std::string error;
|
||||
m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
||||
auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
||||
m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
||||
auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
||||
BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error));
|
||||
|
||||
|
||||
// Reload wallet and make sure new transactions are detected despite events
|
||||
// being blocked
|
||||
wallet = TestLoadWallet(m_node.chain.get());
|
||||
BOOST_CHECK(rescan_completed);
|
||||
BOOST_CHECK_EQUAL(addtx_count, 2);
|
||||
{
|
||||
LOCK(wallet->cs_wallet);
|
||||
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetHash()), 1U);
|
||||
BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetHash()), 1U);
|
||||
}
|
||||
|
||||
|
||||
// Unblock notification queue and make sure stale blockConnected and
|
||||
// transactionAddedToMempool events are processed
|
||||
promise.set_value();
|
||||
SyncWithValidationInterfaceQueue();
|
||||
BOOST_CHECK_EQUAL(addtx_count, 4);
|
||||
|
||||
|
||||
TestUnloadWallet(std::move(wallet));
|
||||
|
||||
|
||||
// Load wallet again, this time creating new block and mempool transactions
|
||||
// paying to the wallet as the wallet finishes loading and syncing the
|
||||
// queue so the events have to be handled immediately. Releasing the wallet
|
||||
// lock during the sync is a little artificial but is needed to avoid a
|
||||
// deadlock during the sync and simulates a new block notification happening
|
||||
// as soon as possible.
|
||||
addtx_count = 0;
|
||||
auto handler = HandleLoadWallet([&](std::unique_ptr<interfaces::Wallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->wallet()->cs_wallet, cs_wallets) {
|
||||
BOOST_CHECK(rescan_completed);
|
||||
m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
||||
block_tx = TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
||||
m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
||||
mempool_tx = TestSimpleSpend(*m_coinbase_txns[3], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
||||
BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error));
|
||||
LEAVE_CRITICAL_SECTION(cs_wallets);
|
||||
LEAVE_CRITICAL_SECTION(wallet->wallet()->cs_wallet);
|
||||
SyncWithValidationInterfaceQueue();
|
||||
ENTER_CRITICAL_SECTION(wallet->wallet()->cs_wallet);
|
||||
ENTER_CRITICAL_SECTION(cs_wallets);
|
||||
});
|
||||
wallet = TestLoadWallet(m_node.chain.get());
|
||||
BOOST_CHECK_EQUAL(addtx_count, 4);
|
||||
{
|
||||
LOCK(wallet->cs_wallet);
|
||||
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetHash()), 1U);
|
||||
BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetHash()), 1U);
|
||||
}
|
||||
|
||||
|
||||
TestUnloadWallet(std::move(wallet));
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup)
|
||||
{
|
||||
auto wallet = TestLoadWallet(nullptr);
|
||||
BOOST_CHECK(wallet);
|
||||
UnloadWallet(std::move(wallet));
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
|
||||
{
|
||||
gArgs.ForceSetArg("-unsafesqlitesync", "1");
|
||||
auto wallet = TestLoadWallet(m_node.chain.get());
|
||||
CKey key;
|
||||
key.MakeNewKey(true);
|
||||
AddKey(*wallet, key);
|
||||
|
||||
std::string error;
|
||||
m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
||||
auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
||||
CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
|
||||
|
||||
SyncWithValidationInterfaceQueue();
|
||||
|
||||
{
|
||||
auto block_hash = block_tx.GetHash();
|
||||
auto prev_hash = m_coinbase_txns[0]->GetHash();
|
||||
|
||||
LOCK(wallet->cs_wallet);
|
||||
BOOST_CHECK(wallet->HasWalletSpend(prev_hash));
|
||||
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u);
|
||||
|
||||
std::vector<uint256> vHashIn{ block_hash }, vHashOut;
|
||||
BOOST_CHECK_EQUAL(wallet->ZapSelectTx(vHashIn, vHashOut), DBErrors::LOAD_OK);
|
||||
|
||||
BOOST_CHECK(!wallet->HasWalletSpend(prev_hash));
|
||||
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u);
|
||||
}
|
||||
|
||||
TestUnloadWallet(std::move(wallet));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
29
src/wallet/test/walletdb_tests.cpp
Normal file
29
src/wallet/test/walletdb_tests.cpp
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2012-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <test/util/setup_common.h>
|
||||
#include <clientversion.h>
|
||||
#include <streams.h>
|
||||
#include <uint256.h>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(walletdb_tests, BasicTestingSetup)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(walletdb_readkeyvalue)
|
||||
{
|
||||
/**
|
||||
* When ReadKeyValue() reads from either a "key" or "wkey" it first reads the CDataStream steam into a
|
||||
* CPrivKey or CWalletKey respectively and then reads a hash of the pubkey and privkey into a uint256.
|
||||
* Wallets from 0.8 or before do not store the pubkey/privkey hash, trying to read the hash from old
|
||||
* wallets throws an exception, for backwards compatibility this read is wrapped in a try block to
|
||||
* silently fail. The test here makes sure the type of exception thrown from CDataStream::read()
|
||||
* matches the type we expect, otherwise we need to update the "key"/"wkey" exception type caught.
|
||||
*/
|
||||
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
|
||||
uint256 dummy;
|
||||
BOOST_CHECK_THROW(ssValue >> dummy, std::ios_base::failure);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
25
src/wallet/transaction.cpp
Normal file
25
src/wallet/transaction.cpp
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) 2021 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <wallet/transaction.h>
|
||||
|
||||
bool CWalletTx::IsEquivalentTo(const CWalletTx& _tx) const
|
||||
{
|
||||
CMutableTransaction tx1 {*this->tx};
|
||||
CMutableTransaction tx2 {*_tx.tx};
|
||||
for (auto& txin : tx1.vin) txin.scriptSig = CScript();
|
||||
for (auto& txin : tx2.vin) txin.scriptSig = CScript();
|
||||
return CTransaction(tx1) == CTransaction(tx2);
|
||||
}
|
||||
|
||||
bool CWalletTx::InMempool() const
|
||||
{
|
||||
return fInMempool;
|
||||
}
|
||||
|
||||
int64_t CWalletTx::GetTxTime() const
|
||||
{
|
||||
int64_t n = nTimeSmart;
|
||||
return n ? n : nTimeReceived;
|
||||
}
|
||||
358
src/wallet/transaction.h
Normal file
358
src/wallet/transaction.h
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
// Copyright (c) 2021 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_TRANSACTION_H
|
||||
#define DIGIBYTE_WALLET_TRANSACTION_H
|
||||
|
||||
#include <amount.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <serialize.h>
|
||||
#include <wallet/ismine.h>
|
||||
#include <threadsafety.h>
|
||||
#include <tinyformat.h>
|
||||
#include <util/strencodings.h>
|
||||
#include <util/string.h>
|
||||
|
||||
#include <list>
|
||||
#include <vector>
|
||||
|
||||
struct COutputEntry;
|
||||
|
||||
typedef std::map<std::string, std::string> mapValue_t;
|
||||
|
||||
//Get the marginal bytes of spending the specified output
|
||||
int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* pwallet, bool use_max_sig = false);
|
||||
|
||||
static inline void ReadOrderPos(int64_t& nOrderPos, mapValue_t& mapValue)
|
||||
{
|
||||
if (!mapValue.count("n"))
|
||||
{
|
||||
nOrderPos = -1; // TODO: calculate elsewhere
|
||||
return;
|
||||
}
|
||||
nOrderPos = atoi64(mapValue["n"]);
|
||||
}
|
||||
|
||||
static inline void WriteOrderPos(const int64_t& nOrderPos, mapValue_t& mapValue)
|
||||
{
|
||||
if (nOrderPos == -1)
|
||||
return;
|
||||
mapValue["n"] = ToString(nOrderPos);
|
||||
}
|
||||
|
||||
/** Legacy class used for deserializing vtxPrev for backwards compatibility.
|
||||
* vtxPrev was removed in commit 93a18a3650292afbb441a47d1fa1b94aeb0164e3,
|
||||
* but old wallet.dat files may still contain vtxPrev vectors of CMerkleTxs.
|
||||
* These need to get deserialized for field alignment when deserializing
|
||||
* a CWalletTx, but the deserialized values are discarded.**/
|
||||
class CMerkleTx
|
||||
{
|
||||
public:
|
||||
template<typename Stream>
|
||||
void Unserialize(Stream& s)
|
||||
{
|
||||
CTransactionRef tx;
|
||||
uint256 hashBlock;
|
||||
std::vector<uint256> vMerkleBranch;
|
||||
int nIndex;
|
||||
|
||||
s >> tx >> hashBlock >> vMerkleBranch >> nIndex;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A transaction with a bunch of additional info that only the owner cares about.
|
||||
* It includes any unrecorded transactions needed to link it back to the block chain.
|
||||
*/
|
||||
class CWalletTx
|
||||
{
|
||||
private:
|
||||
const CWallet* const pwallet;
|
||||
|
||||
/** Constant used in hashBlock to indicate tx has been abandoned, only used at
|
||||
* serialization/deserialization to avoid ambiguity with conflicted.
|
||||
*/
|
||||
static constexpr const uint256& ABANDON_HASH = uint256::ONE;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Key/value map with information about the transaction.
|
||||
*
|
||||
* The following keys can be read and written through the map and are
|
||||
* serialized in the wallet database:
|
||||
*
|
||||
* "comment", "to" - comment strings provided to sendtoaddress,
|
||||
* and sendmany wallet RPCs
|
||||
* "replaces_txid" - txid (as HexStr) of transaction replaced by
|
||||
* bumpfee on transaction created by bumpfee
|
||||
* "replaced_by_txid" - txid (as HexStr) of transaction created by
|
||||
* bumpfee on transaction replaced by bumpfee
|
||||
* "from", "message" - obsolete fields that could be set in UI prior to
|
||||
* 2011 (removed in commit 4d9b223)
|
||||
*
|
||||
* The following keys are serialized in the wallet database, but shouldn't
|
||||
* be read or written through the map (they will be temporarily added and
|
||||
* removed from the map during serialization):
|
||||
*
|
||||
* "fromaccount" - serialized strFromAccount value
|
||||
* "n" - serialized nOrderPos value
|
||||
* "timesmart" - serialized nTimeSmart value
|
||||
* "spent" - serialized vfSpent value that existed prior to
|
||||
* 2014 (removed in commit 93a18a3)
|
||||
*/
|
||||
mapValue_t mapValue;
|
||||
std::vector<std::pair<std::string, std::string> > vOrderForm;
|
||||
unsigned int fTimeReceivedIsTxTime;
|
||||
unsigned int nTimeReceived; //!< time received by this node
|
||||
/**
|
||||
* Stable timestamp that never changes, and reflects the order a transaction
|
||||
* was added to the wallet. Timestamp is based on the block time for a
|
||||
* transaction added as part of a block, or else the time when the
|
||||
* transaction was received if it wasn't part of a block, with the timestamp
|
||||
* adjusted in both cases so timestamp order matches the order transactions
|
||||
* were added to the wallet. More details can be found in
|
||||
* CWallet::ComputeTimeSmart().
|
||||
*/
|
||||
unsigned int nTimeSmart;
|
||||
/**
|
||||
* From me flag is set to 1 for transactions that were created by the wallet
|
||||
* on this Vexta node, and set to 0 for transactions that were created
|
||||
* externally and came in through the network or sendrawtransaction RPC.
|
||||
*/
|
||||
bool fFromMe;
|
||||
int64_t nOrderPos; //!< position in ordered transaction list
|
||||
std::multimap<int64_t, CWalletTx*>::const_iterator m_it_wtxOrdered;
|
||||
|
||||
// memory only
|
||||
enum AmountType { DEBIT, CREDIT, IMMATURE_CREDIT, AVAILABLE_CREDIT, AMOUNTTYPE_ENUM_ELEMENTS };
|
||||
CAmount GetCachableAmount(AmountType type, const isminefilter& filter, bool recalculate = false) const;
|
||||
mutable CachableAmount m_amounts[AMOUNTTYPE_ENUM_ELEMENTS];
|
||||
/**
|
||||
* This flag is true if all m_amounts caches are empty. This is particularly
|
||||
* useful in places where MarkDirty is conditionally called and the
|
||||
* condition can be expensive and thus can be skipped if the flag is true.
|
||||
* See MarkDestinationsDirty.
|
||||
*/
|
||||
mutable bool m_is_cache_empty{true};
|
||||
mutable bool fChangeCached;
|
||||
mutable bool fInMempool;
|
||||
mutable CAmount nChangeCached;
|
||||
|
||||
CWalletTx(const CWallet* wallet, CTransactionRef arg)
|
||||
: pwallet(wallet),
|
||||
tx(std::move(arg))
|
||||
{
|
||||
Init();
|
||||
}
|
||||
|
||||
void Init()
|
||||
{
|
||||
mapValue.clear();
|
||||
vOrderForm.clear();
|
||||
fTimeReceivedIsTxTime = false;
|
||||
nTimeReceived = 0;
|
||||
nTimeSmart = 0;
|
||||
fFromMe = false;
|
||||
fChangeCached = false;
|
||||
fInMempool = false;
|
||||
nChangeCached = 0;
|
||||
nOrderPos = -1;
|
||||
m_confirm = Confirmation{};
|
||||
}
|
||||
|
||||
CTransactionRef tx;
|
||||
|
||||
/** New transactions start as UNCONFIRMED. At BlockConnected,
|
||||
* they will transition to CONFIRMED. In case of reorg, at BlockDisconnected,
|
||||
* they roll back to UNCONFIRMED. If we detect a conflicting transaction at
|
||||
* block connection, we update conflicted tx and its dependencies as CONFLICTED.
|
||||
* If tx isn't confirmed and outside of mempool, the user may switch it to ABANDONED
|
||||
* by using the abandontransaction call. This last status may be override by a CONFLICTED
|
||||
* or CONFIRMED transition.
|
||||
*/
|
||||
enum Status {
|
||||
UNCONFIRMED,
|
||||
CONFIRMED,
|
||||
CONFLICTED,
|
||||
ABANDONED
|
||||
};
|
||||
|
||||
/** Confirmation includes tx status and a triplet of {block height/block hash/tx index in block}
|
||||
* at which tx has been confirmed. All three are set to 0 if tx is unconfirmed or abandoned.
|
||||
* Meaning of these fields changes with CONFLICTED state where they instead point to block hash
|
||||
* and block height of the deepest conflicting tx.
|
||||
*/
|
||||
struct Confirmation {
|
||||
Status status;
|
||||
int block_height;
|
||||
uint256 hashBlock;
|
||||
int nIndex;
|
||||
Confirmation(Status s = UNCONFIRMED, int b = 0, uint256 h = uint256(), int i = 0) : status(s), block_height(b), hashBlock(h), nIndex(i) {}
|
||||
};
|
||||
|
||||
Confirmation m_confirm;
|
||||
|
||||
template<typename Stream>
|
||||
void Serialize(Stream& s) const
|
||||
{
|
||||
mapValue_t mapValueCopy = mapValue;
|
||||
|
||||
mapValueCopy["fromaccount"] = "";
|
||||
WriteOrderPos(nOrderPos, mapValueCopy);
|
||||
if (nTimeSmart) {
|
||||
mapValueCopy["timesmart"] = strprintf("%u", nTimeSmart);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> dummy_vector1; //!< Used to be vMerkleBranch
|
||||
std::vector<uint8_t> dummy_vector2; //!< Used to be vtxPrev
|
||||
bool dummy_bool = false; //!< Used to be fSpent
|
||||
uint256 serializedHash = isAbandoned() ? ABANDON_HASH : m_confirm.hashBlock;
|
||||
int serializedIndex = isAbandoned() || isConflicted() ? -1 : m_confirm.nIndex;
|
||||
s << tx << serializedHash << dummy_vector1 << serializedIndex << dummy_vector2 << mapValueCopy << vOrderForm << fTimeReceivedIsTxTime << nTimeReceived << fFromMe << dummy_bool;
|
||||
}
|
||||
|
||||
template<typename Stream>
|
||||
void Unserialize(Stream& s)
|
||||
{
|
||||
Init();
|
||||
|
||||
std::vector<uint256> dummy_vector1; //!< Used to be vMerkleBranch
|
||||
std::vector<CMerkleTx> dummy_vector2; //!< Used to be vtxPrev
|
||||
bool dummy_bool; //! Used to be fSpent
|
||||
int serializedIndex;
|
||||
s >> tx >> m_confirm.hashBlock >> dummy_vector1 >> serializedIndex >> dummy_vector2 >> mapValue >> vOrderForm >> fTimeReceivedIsTxTime >> nTimeReceived >> fFromMe >> dummy_bool;
|
||||
|
||||
/* At serialization/deserialization, an nIndex == -1 means that hashBlock refers to
|
||||
* the earliest block in the chain we know this or any in-wallet ancestor conflicts
|
||||
* with. If nIndex == -1 and hashBlock is ABANDON_HASH, it means transaction is abandoned.
|
||||
* In same context, an nIndex >= 0 refers to a confirmed transaction (if hashBlock set) or
|
||||
* unconfirmed one. Older clients interpret nIndex == -1 as unconfirmed for backward
|
||||
* compatibility (pre-commit 9ac63d6).
|
||||
*/
|
||||
if (serializedIndex == -1 && m_confirm.hashBlock == ABANDON_HASH) {
|
||||
setAbandoned();
|
||||
} else if (serializedIndex == -1) {
|
||||
setConflicted();
|
||||
} else if (!m_confirm.hashBlock.IsNull()) {
|
||||
m_confirm.nIndex = serializedIndex;
|
||||
setConfirmed();
|
||||
}
|
||||
|
||||
ReadOrderPos(nOrderPos, mapValue);
|
||||
nTimeSmart = mapValue.count("timesmart") ? (unsigned int)atoi64(mapValue["timesmart"]) : 0;
|
||||
|
||||
mapValue.erase("fromaccount");
|
||||
mapValue.erase("spent");
|
||||
mapValue.erase("n");
|
||||
mapValue.erase("timesmart");
|
||||
}
|
||||
|
||||
void SetTx(CTransactionRef arg)
|
||||
{
|
||||
tx = std::move(arg);
|
||||
}
|
||||
|
||||
//! make sure balances are recalculated
|
||||
void MarkDirty()
|
||||
{
|
||||
m_amounts[DEBIT].Reset();
|
||||
m_amounts[CREDIT].Reset();
|
||||
m_amounts[IMMATURE_CREDIT].Reset();
|
||||
m_amounts[AVAILABLE_CREDIT].Reset();
|
||||
fChangeCached = false;
|
||||
m_is_cache_empty = true;
|
||||
}
|
||||
|
||||
//! filter decides which addresses will count towards the debit
|
||||
CAmount GetDebit(const isminefilter& filter) const;
|
||||
CAmount GetCredit(const isminefilter& filter) const;
|
||||
CAmount GetImmatureCredit(bool fUseCache = true) const;
|
||||
// TODO: Remove "NO_THREAD_SAFETY_ANALYSIS" and replace it with the correct
|
||||
// annotation "EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)". The
|
||||
// annotation "NO_THREAD_SAFETY_ANALYSIS" was temporarily added to avoid
|
||||
// having to resolve the issue of member access into incomplete type CWallet.
|
||||
CAmount GetAvailableCredit(bool fUseCache = true, const isminefilter& filter = ISMINE_SPENDABLE) const NO_THREAD_SAFETY_ANALYSIS;
|
||||
CAmount GetImmatureWatchOnlyCredit(const bool fUseCache = true) const;
|
||||
CAmount GetChange() const;
|
||||
|
||||
/** Get the marginal bytes if spending the specified output from this transaction */
|
||||
int GetSpendSize(unsigned int out, bool use_max_sig = false) const
|
||||
{
|
||||
return CalculateMaximumSignedInputSize(tx->vout[out], pwallet, use_max_sig);
|
||||
}
|
||||
|
||||
void GetAmounts(std::list<COutputEntry>& listReceived,
|
||||
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter) const;
|
||||
|
||||
bool IsFromMe(const isminefilter& filter) const
|
||||
{
|
||||
return (GetDebit(filter) > 0);
|
||||
}
|
||||
|
||||
/** True if only scriptSigs are different */
|
||||
bool IsEquivalentTo(const CWalletTx& tx) const;
|
||||
|
||||
bool InMempool() const;
|
||||
bool IsTrusted() const;
|
||||
|
||||
int64_t GetTxTime() const;
|
||||
|
||||
/** Pass this transaction to node for mempool insertion and relay to peers if flag set to true */
|
||||
bool SubmitMemoryPoolAndRelay(std::string& err_string, bool relay);
|
||||
|
||||
// TODO: Remove "NO_THREAD_SAFETY_ANALYSIS" and replace it with the correct
|
||||
// annotation "EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)". The annotation
|
||||
// "NO_THREAD_SAFETY_ANALYSIS" was temporarily added to avoid having to
|
||||
// resolve the issue of member access into incomplete type CWallet. Note
|
||||
// that we still have the runtime check "AssertLockHeld(pwallet->cs_wallet)"
|
||||
// in place.
|
||||
std::set<uint256> GetConflicts() const NO_THREAD_SAFETY_ANALYSIS;
|
||||
|
||||
/**
|
||||
* Return depth of transaction in blockchain:
|
||||
* <0 : conflicts with a transaction this deep in the blockchain
|
||||
* 0 : in memory pool, waiting to be included in a block
|
||||
* >=1 : this many blocks deep in the main chain
|
||||
*/
|
||||
// TODO: Remove "NO_THREAD_SAFETY_ANALYSIS" and replace it with the correct
|
||||
// annotation "EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)". The annotation
|
||||
// "NO_THREAD_SAFETY_ANALYSIS" was temporarily added to avoid having to
|
||||
// resolve the issue of member access into incomplete type CWallet. Note
|
||||
// that we still have the runtime check "AssertLockHeld(pwallet->cs_wallet)"
|
||||
// in place.
|
||||
int GetDepthInMainChain() const NO_THREAD_SAFETY_ANALYSIS;
|
||||
bool IsInMainChain() const { return GetDepthInMainChain() > 0; }
|
||||
|
||||
/**
|
||||
* @return number of blocks to maturity for this transaction:
|
||||
* 0 : is not a coinbase transaction, or is a mature coinbase transaction
|
||||
* >0 : is a coinbase transaction which matures in this many blocks
|
||||
*/
|
||||
int GetBlocksToMaturity() const;
|
||||
bool isAbandoned() const { return m_confirm.status == CWalletTx::ABANDONED; }
|
||||
void setAbandoned()
|
||||
{
|
||||
m_confirm.status = CWalletTx::ABANDONED;
|
||||
m_confirm.hashBlock = uint256();
|
||||
m_confirm.block_height = 0;
|
||||
m_confirm.nIndex = 0;
|
||||
}
|
||||
bool isConflicted() const { return m_confirm.status == CWalletTx::CONFLICTED; }
|
||||
void setConflicted() { m_confirm.status = CWalletTx::CONFLICTED; }
|
||||
bool isUnconfirmed() const { return m_confirm.status == CWalletTx::UNCONFIRMED; }
|
||||
void setUnconfirmed() { m_confirm.status = CWalletTx::UNCONFIRMED; }
|
||||
bool isConfirmed() const { return m_confirm.status == CWalletTx::CONFIRMED; }
|
||||
void setConfirmed() { m_confirm.status = CWalletTx::CONFIRMED; }
|
||||
const uint256& GetHash() const { return tx->GetHash(); }
|
||||
bool IsCoinBase() const { return tx->IsCoinBase(); }
|
||||
bool IsImmatureCoinBase() const;
|
||||
|
||||
// Disable copying of CWalletTx objects to prevent bugs where instances get
|
||||
// copied in and out of the mapWallet map, and fields are updated in the
|
||||
// wrong copy.
|
||||
CWalletTx(CWalletTx const &) = delete;
|
||||
void operator=(CWalletTx const &x) = delete;
|
||||
};
|
||||
|
||||
#endif // DIGIBYTE_WALLET_TRANSACTION_H
|
||||
3295
src/wallet/wallet.cpp
Normal file
3295
src/wallet/wallet.cpp
Normal file
File diff suppressed because it is too large
Load diff
979
src/wallet/wallet.h
Normal file
979
src/wallet/wallet.h
Normal file
|
|
@ -0,0 +1,979 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_WALLET_H
|
||||
#define DIGIBYTE_WALLET_WALLET_H
|
||||
|
||||
#include <amount.h>
|
||||
#include <interfaces/chain.h>
|
||||
#include <interfaces/handler.h>
|
||||
#include <outputtype.h>
|
||||
#include <policy/feerate.h>
|
||||
#include <psbt.h>
|
||||
#include <tinyformat.h>
|
||||
#include <util/message.h>
|
||||
#include <util/strencodings.h>
|
||||
#include <util/string.h>
|
||||
#include <util/system.h>
|
||||
#include <util/ui_change_type.h>
|
||||
#include <validationinterface.h>
|
||||
#include <wallet/coinselection.h>
|
||||
#include <wallet/crypter.h>
|
||||
#include <wallet/receive.h>
|
||||
#include <wallet/scriptpubkeyman.h>
|
||||
#include <wallet/spend.h>
|
||||
#include <wallet/transaction.h>
|
||||
#include <wallet/walletdb.h>
|
||||
#include <wallet/walletutil.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/signals2/signal.hpp>
|
||||
|
||||
using LoadWalletFn = std::function<void(std::unique_ptr<interfaces::Wallet> wallet)>;
|
||||
|
||||
struct bilingual_str;
|
||||
|
||||
//! Explicitly unload and delete the wallet.
|
||||
//! Blocks the current thread after signaling the unload intent so that all
|
||||
//! wallet clients release the wallet.
|
||||
//! Note that, when blocking is not required, the wallet is implicitly unloaded
|
||||
//! by the shared pointer deleter.
|
||||
void UnloadWallet(std::shared_ptr<CWallet>&& wallet);
|
||||
|
||||
bool AddWallet(const std::shared_ptr<CWallet>& wallet);
|
||||
bool RemoveWallet(const std::shared_ptr<CWallet>& wallet, std::optional<bool> load_on_start, std::vector<bilingual_str>& warnings);
|
||||
bool RemoveWallet(const std::shared_ptr<CWallet>& wallet, std::optional<bool> load_on_start);
|
||||
std::vector<std::shared_ptr<CWallet>> GetWallets();
|
||||
std::shared_ptr<CWallet> GetWallet(const std::string& name);
|
||||
std::shared_ptr<CWallet> LoadWallet(interfaces::Chain& chain, const std::string& name, std::optional<bool> load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings);
|
||||
std::shared_ptr<CWallet> CreateWallet(interfaces::Chain& chain, const std::string& name, std::optional<bool> load_on_start, DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings);
|
||||
std::unique_ptr<interfaces::Handler> HandleLoadWallet(LoadWalletFn load_wallet);
|
||||
std::unique_ptr<WalletDatabase> MakeWalletDatabase(const std::string& name, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error);
|
||||
|
||||
//! -paytxfee default
|
||||
constexpr CAmount DEFAULT_PAY_TX_FEE = 0;
|
||||
//! -fallbackfee default
|
||||
static const CAmount DEFAULT_FALLBACK_FEE = 0;
|
||||
//! -discardfee default
|
||||
static const CAmount DEFAULT_DISCARD_FEE = 10000;
|
||||
//! -mintxfee default
|
||||
static const CAmount DEFAULT_TRANSACTION_MINFEE = 10000000;
|
||||
/**
|
||||
* maximum fee increase allowed to do partial spend avoidance, even for nodes with this feature disabled by default
|
||||
*
|
||||
* A value of -1 disables this feature completely.
|
||||
* A value of 0 (current default) means to attempt to do partial spend avoidance, and use its results if the fees remain *unchanged*
|
||||
* A value > 0 means to do partial spend avoidance if the fee difference against a regular coin selection instance is in the range [0..value].
|
||||
*/
|
||||
static const CAmount DEFAULT_MAX_AVOIDPARTIALSPEND_FEE = 0;
|
||||
//! discourage APS fee higher than this amount
|
||||
constexpr CAmount HIGH_APS_FEE{COIN / 100000000};
|
||||
//! minimum recommended increment for BIP 125 replacement txs
|
||||
static const CAmount WALLET_INCREMENTAL_RELAY_FEE = 1000000;
|
||||
//! Default for -spendzeroconfchange
|
||||
static const bool DEFAULT_SPEND_ZEROCONF_CHANGE = true;
|
||||
//! Default for -walletrejectlongchains
|
||||
static const bool DEFAULT_WALLET_REJECT_LONG_CHAINS = false;
|
||||
//! -txconfirmtarget default
|
||||
static const unsigned int DEFAULT_TX_CONFIRM_TARGET = 6;
|
||||
//! -walletrbf default
|
||||
static const bool DEFAULT_WALLET_RBF = false;
|
||||
static const bool DEFAULT_WALLETBROADCAST = true;
|
||||
static const bool DEFAULT_DISABLE_WALLET = false;
|
||||
static const bool DEFAULT_DISABLE_DANDELION = false;
|
||||
|
||||
//! -maxtxfee default
|
||||
constexpr CAmount DEFAULT_TRANSACTION_MAXFEE{COIN * 100};
|
||||
//! Discourage users to set fees higher than this amount (in satoshis) per kB
|
||||
constexpr CAmount HIGH_TX_FEE_PER_KB{COIN};
|
||||
//! -maxtxfee will warn if called with a higher fee than this amount (in satoshis)
|
||||
constexpr CAmount HIGH_MAX_TX_FEE{1000 * HIGH_TX_FEE_PER_KB};
|
||||
//! Pre-calculated constants for input size estimation in *virtual size*
|
||||
static constexpr size_t DUMMY_NESTED_P2WPKH_INPUT_SIZE = 91;
|
||||
|
||||
class CCoinControl;
|
||||
class COutput;
|
||||
class CScript;
|
||||
class CWalletTx;
|
||||
struct FeeCalculation;
|
||||
enum class FeeEstimateMode;
|
||||
class ReserveDestination;
|
||||
|
||||
//! Default for -addresstype
|
||||
constexpr OutputType DEFAULT_ADDRESS_TYPE{OutputType::BECH32};
|
||||
|
||||
static constexpr uint64_t KNOWN_WALLET_FLAGS =
|
||||
WALLET_FLAG_AVOID_REUSE
|
||||
| WALLET_FLAG_BLANK_WALLET
|
||||
| WALLET_FLAG_KEY_ORIGIN_METADATA
|
||||
| WALLET_FLAG_LAST_HARDENED_XPUB_CACHED
|
||||
| WALLET_FLAG_DISABLE_PRIVATE_KEYS
|
||||
| WALLET_FLAG_DESCRIPTORS
|
||||
| WALLET_FLAG_EXTERNAL_SIGNER;
|
||||
|
||||
static constexpr uint64_t MUTABLE_WALLET_FLAGS =
|
||||
WALLET_FLAG_AVOID_REUSE;
|
||||
|
||||
static const std::map<std::string,WalletFlags> WALLET_FLAG_MAP{
|
||||
{"avoid_reuse", WALLET_FLAG_AVOID_REUSE},
|
||||
{"blank", WALLET_FLAG_BLANK_WALLET},
|
||||
{"key_origin_metadata", WALLET_FLAG_KEY_ORIGIN_METADATA},
|
||||
{"last_hardened_xpub_cached", WALLET_FLAG_LAST_HARDENED_XPUB_CACHED},
|
||||
{"disable_private_keys", WALLET_FLAG_DISABLE_PRIVATE_KEYS},
|
||||
{"descriptor_wallet", WALLET_FLAG_DESCRIPTORS},
|
||||
{"external_signer", WALLET_FLAG_EXTERNAL_SIGNER}
|
||||
};
|
||||
|
||||
extern const std::map<uint64_t,std::string> WALLET_FLAG_CAVEATS;
|
||||
|
||||
/** A wrapper to reserve an address from a wallet
|
||||
*
|
||||
* ReserveDestination is used to reserve an address.
|
||||
* It is currently only used inside of CreateTransaction.
|
||||
*
|
||||
* Instantiating a ReserveDestination does not reserve an address. To do so,
|
||||
* GetReservedDestination() needs to be called on the object. Once an address has been
|
||||
* reserved, call KeepDestination() on the ReserveDestination object to make sure it is not
|
||||
* returned. Call ReturnDestination() to return the address so it can be re-used (for
|
||||
* example, if the address was used in a new transaction
|
||||
* and that transaction was not completed and needed to be aborted).
|
||||
*
|
||||
* If an address is reserved and KeepDestination() is not called, then the address will be
|
||||
* returned when the ReserveDestination goes out of scope.
|
||||
*/
|
||||
class ReserveDestination
|
||||
{
|
||||
protected:
|
||||
//! The wallet to reserve from
|
||||
const CWallet* const pwallet;
|
||||
//! The ScriptPubKeyMan to reserve from. Based on type when GetReservedDestination is called
|
||||
ScriptPubKeyMan* m_spk_man{nullptr};
|
||||
OutputType const type;
|
||||
//! The index of the address's key in the keypool
|
||||
int64_t nIndex{-1};
|
||||
//! The destination
|
||||
CTxDestination address;
|
||||
//! Whether this is from the internal (change output) keypool
|
||||
bool fInternal{false};
|
||||
|
||||
public:
|
||||
//! Construct a ReserveDestination object. This does NOT reserve an address yet
|
||||
explicit ReserveDestination(CWallet* pwallet, OutputType type)
|
||||
: pwallet(pwallet)
|
||||
, type(type) { }
|
||||
|
||||
ReserveDestination(const ReserveDestination&) = delete;
|
||||
ReserveDestination& operator=(const ReserveDestination&) = delete;
|
||||
|
||||
//! Destructor. If a key has been reserved and not KeepKey'ed, it will be returned to the keypool
|
||||
~ReserveDestination()
|
||||
{
|
||||
ReturnDestination();
|
||||
}
|
||||
|
||||
//! Reserve an address
|
||||
bool GetReservedDestination(CTxDestination& pubkey, bool internal, std::string& error);
|
||||
//! Return reserved address
|
||||
void ReturnDestination();
|
||||
//! Keep the address. Do not return it's key to the keypool when this object goes out of scope
|
||||
void KeepDestination();
|
||||
};
|
||||
|
||||
/** Address book data */
|
||||
class CAddressBookData
|
||||
{
|
||||
private:
|
||||
bool m_change{true};
|
||||
std::string m_label;
|
||||
public:
|
||||
std::string purpose;
|
||||
|
||||
CAddressBookData() : purpose("unknown") {}
|
||||
|
||||
typedef std::map<std::string, std::string> StringMap;
|
||||
StringMap destdata;
|
||||
|
||||
bool IsChange() const { return m_change; }
|
||||
const std::string& GetLabel() const { return m_label; }
|
||||
void SetLabel(const std::string& label) {
|
||||
m_change = false;
|
||||
m_label = label;
|
||||
}
|
||||
};
|
||||
|
||||
struct CRecipient
|
||||
{
|
||||
CScript scriptPubKey;
|
||||
CAmount nAmount;
|
||||
bool fSubtractFeeFromAmount;
|
||||
};
|
||||
|
||||
class WalletRescanReserver; //forward declarations for ScanForWalletTransactions/RescanFromTime
|
||||
/**
|
||||
* A CWallet maintains a set of transactions and balances, and provides the ability to create new transactions.
|
||||
*/
|
||||
class CWallet final : public WalletStorage, public interfaces::Chain::Notifications
|
||||
{
|
||||
private:
|
||||
CKeyingMaterial vMasterKey GUARDED_BY(cs_wallet);
|
||||
|
||||
bool Unlock(const CKeyingMaterial& vMasterKeyIn, bool accept_no_keys = false);
|
||||
|
||||
std::atomic<bool> fAbortRescan{false};
|
||||
std::atomic<bool> fScanningWallet{false}; // controlled by WalletRescanReserver
|
||||
std::atomic<int64_t> m_scanning_start{0};
|
||||
std::atomic<double> m_scanning_progress{0};
|
||||
friend class WalletRescanReserver;
|
||||
|
||||
//! the current wallet version: clients below this version are not able to load the wallet
|
||||
int nWalletVersion GUARDED_BY(cs_wallet){FEATURE_BASE};
|
||||
|
||||
/** The next scheduled rebroadcast of wallet transactions. */
|
||||
int64_t nNextResend = 0;
|
||||
/** Whether this wallet will submit newly created transactions to the node's mempool and
|
||||
* prompt rebroadcasts (see ResendWalletTransactions()). */
|
||||
bool fBroadcastTransactions = false;
|
||||
// Local time that the tip block was received. Used to schedule wallet rebroadcasts.
|
||||
std::atomic<int64_t> m_best_block_time {0};
|
||||
|
||||
/**
|
||||
* Used to keep track of spent outpoints, and
|
||||
* detect and report conflicts (double-spends or
|
||||
* mutated transactions where the mutant gets mined).
|
||||
*/
|
||||
typedef std::multimap<COutPoint, uint256> TxSpends;
|
||||
TxSpends mapTxSpends GUARDED_BY(cs_wallet);
|
||||
void AddToSpends(const COutPoint& outpoint, const uint256& wtxid) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void AddToSpends(const uint256& wtxid) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
/**
|
||||
* Add a transaction to the wallet, or update it. pIndex and posInBlock should
|
||||
* be set when the transaction was known to be included in a block. When
|
||||
* pIndex == nullptr, then wallet state is not updated in AddToWallet, but
|
||||
* notifications happen and cached balances are marked dirty.
|
||||
*
|
||||
* If fUpdate is true, existing transactions will be updated.
|
||||
* TODO: One exception to this is that the abandoned state is cleared under the
|
||||
* assumption that any further notification of a transaction that was considered
|
||||
* abandoned is an indication that it is not safe to be considered abandoned.
|
||||
* Abandoned state should probably be more carefully tracked via different
|
||||
* posInBlock signals or by checking mempool presence when necessary.
|
||||
*/
|
||||
bool AddToWalletIfInvolvingMe(const CTransactionRef& tx, CWalletTx::Confirmation confirm, bool fUpdate) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
/** Mark a transaction (and its in-wallet descendants) as conflicting with a particular block. */
|
||||
void MarkConflicted(const uint256& hashBlock, int conflicting_height, const uint256& hashTx);
|
||||
|
||||
/** Mark a transaction's inputs dirty, thus forcing the outputs to be recomputed */
|
||||
void MarkInputsDirty(const CTransactionRef& tx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
void SyncMetaData(std::pair<TxSpends::iterator, TxSpends::iterator>) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
/* Used by TransactionAddedToMemorypool/BlockConnected/Disconnected/ScanForWalletTransactions.
|
||||
* Should be called with non-zero block_hash and posInBlock if this is for a transaction that is included in a block. */
|
||||
void SyncTransaction(const CTransactionRef& tx, CWalletTx::Confirmation confirm, bool update_tx = true) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
/** WalletFlags set on this wallet. */
|
||||
std::atomic<uint64_t> m_wallet_flags{0};
|
||||
|
||||
bool SetAddressBookWithDB(WalletBatch& batch, const CTxDestination& address, const std::string& strName, const std::string& strPurpose);
|
||||
|
||||
//! Unsets a wallet flag and saves it to disk
|
||||
void UnsetWalletFlagWithDB(WalletBatch& batch, uint64_t flag);
|
||||
|
||||
//! Unset the blank wallet flag and saves it to disk
|
||||
void UnsetBlankWalletFlag(WalletBatch& batch) override;
|
||||
|
||||
/** Interface for accessing chain state. */
|
||||
interfaces::Chain* m_chain;
|
||||
|
||||
/** Wallet name: relative directory name or "" for default wallet. */
|
||||
std::string m_name;
|
||||
|
||||
/** Internal database handle. */
|
||||
std::unique_ptr<WalletDatabase> const m_database;
|
||||
|
||||
/**
|
||||
* The following is used to keep track of how far behind the wallet is
|
||||
* from the chain sync, and to allow clients to block on us being caught up.
|
||||
*
|
||||
* Processed hash is a pointer on node's tip and doesn't imply that the wallet
|
||||
* has scanned sequentially all blocks up to this one.
|
||||
*/
|
||||
uint256 m_last_block_processed GUARDED_BY(cs_wallet);
|
||||
|
||||
/** Height of last block processed is used by wallet to know depth of transactions
|
||||
* without relying on Chain interface beyond asynchronous updates. For safety, we
|
||||
* initialize it to -1. Height is a pointer on node's tip and doesn't imply
|
||||
* that the wallet has scanned sequentially all blocks up to this one.
|
||||
*/
|
||||
int m_last_block_processed_height GUARDED_BY(cs_wallet) = -1;
|
||||
|
||||
std::map<OutputType, ScriptPubKeyMan*> m_external_spk_managers;
|
||||
std::map<OutputType, ScriptPubKeyMan*> m_internal_spk_managers;
|
||||
|
||||
// Indexed by a unique identifier produced by each ScriptPubKeyMan using
|
||||
// ScriptPubKeyMan::GetID. In many cases it will be the hash of an internal structure
|
||||
std::map<uint256, std::unique_ptr<ScriptPubKeyMan>> m_spk_managers;
|
||||
|
||||
bool CreateTransactionInternal(const std::vector<CRecipient>& vecSend, CTransactionRef& tx, CAmount& nFeeRet, int& nChangePosInOut, bilingual_str& error, const CCoinControl& coin_control, FeeCalculation& fee_calc_out, bool sign) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
/**
|
||||
* Catch wallet up to current chain, scanning new blocks, updating the best
|
||||
* block locator and m_last_block_processed, and registering for
|
||||
* notifications about new blocks and transactions.
|
||||
*/
|
||||
static bool AttachChain(const std::shared_ptr<CWallet>& wallet, interfaces::Chain& chain, bilingual_str& error, std::vector<bilingual_str>& warnings);
|
||||
|
||||
public:
|
||||
/**
|
||||
* Main wallet lock.
|
||||
* This lock protects all the fields added by CWallet.
|
||||
*/
|
||||
mutable RecursiveMutex cs_wallet;
|
||||
|
||||
WalletDatabase& GetDatabase() const override
|
||||
{
|
||||
assert(static_cast<bool>(m_database));
|
||||
return *m_database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a set of coins such that nValueRet >= nTargetValue and at least
|
||||
* all coins from coin_control are selected; never select unconfirmed coins if they are not ours
|
||||
* param@[out] setCoinsRet Populated with inputs including pre-selected inputs from
|
||||
* coin_control and Coin Selection if successful.
|
||||
* param@[out] nValueRet Total value of selected coins including pre-selected ones
|
||||
* from coin_control and Coin Selection if successful.
|
||||
*/
|
||||
bool SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAmount& nTargetValue, std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet,
|
||||
const CCoinControl& coin_control, CoinSelectionParams& coin_selection_params) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
/** Get a name for this wallet for logging/debugging purposes.
|
||||
*/
|
||||
const std::string& GetName() const { return m_name; }
|
||||
|
||||
typedef std::map<unsigned int, CMasterKey> MasterKeyMap;
|
||||
MasterKeyMap mapMasterKeys;
|
||||
unsigned int nMasterKeyMaxID = 0;
|
||||
|
||||
/** Construct wallet with specified name and database implementation. */
|
||||
CWallet(interfaces::Chain* chain, const std::string& name, std::unique_ptr<WalletDatabase> database)
|
||||
: m_chain(chain),
|
||||
m_name(name),
|
||||
m_database(std::move(database))
|
||||
{
|
||||
}
|
||||
|
||||
~CWallet()
|
||||
{
|
||||
// Should not have slots connected at this point.
|
||||
assert(NotifyUnload.empty());
|
||||
}
|
||||
|
||||
bool IsCrypted() const;
|
||||
bool IsLocked() const override;
|
||||
bool Lock();
|
||||
|
||||
/** Interface to assert chain access */
|
||||
bool HaveChain() const { return m_chain ? true : false; }
|
||||
|
||||
/** Map from txid to CWalletTx for all transactions this wallet is
|
||||
* interested in, including received and sent transactions. */
|
||||
std::map<uint256, CWalletTx> mapWallet GUARDED_BY(cs_wallet);
|
||||
|
||||
typedef std::multimap<int64_t, CWalletTx*> TxItems;
|
||||
TxItems wtxOrdered;
|
||||
|
||||
int64_t nOrderPosNext GUARDED_BY(cs_wallet) = 0;
|
||||
uint64_t nAccountingEntryNumber = 0;
|
||||
|
||||
std::map<CTxDestination, CAddressBookData> m_address_book GUARDED_BY(cs_wallet);
|
||||
const CAddressBookData* FindAddressBookEntry(const CTxDestination&, bool allow_change = false) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
/** Set of Coins owned by this wallet that we won't try to spend from. A
|
||||
* Coin may be locked if it has already been used to fund a transaction
|
||||
* that hasn't confirmed yet. We wouldn't consider the Coin spent already,
|
||||
* but also shouldn't try to use it again. */
|
||||
std::set<COutPoint> setLockedCoins GUARDED_BY(cs_wallet);
|
||||
|
||||
/** Registered interfaces::Chain::Notifications handler. */
|
||||
std::unique_ptr<interfaces::Handler> m_chain_notifications_handler;
|
||||
|
||||
/** Interface for accessing chain state. */
|
||||
interfaces::Chain& chain() const { assert(m_chain); return *m_chain; }
|
||||
|
||||
const CWalletTx* GetWalletTx(const uint256& hash) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
bool IsTrusted(const CWalletTx& wtx, std::set<uint256>& trusted_parents) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
//! check whether we support the named feature
|
||||
bool CanSupportFeature(enum WalletFeature wf) const override EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); return IsFeatureSupported(nWalletVersion, wf); }
|
||||
|
||||
/**
|
||||
* populate vCoins with vector of available COutputs.
|
||||
*/
|
||||
void AvailableCoins(std::vector<COutput>& vCoins, const CCoinControl* coinControl = nullptr, const CAmount& nMinimumAmount = 1, const CAmount& nMaximumAmount = MAX_MONEY, const CAmount& nMinimumSumAmount = MAX_MONEY, const uint64_t nMaximumCount = 0) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
/**
|
||||
* Return list of available coins and locked coins grouped by non-change output address.
|
||||
*/
|
||||
std::map<CTxDestination, std::vector<COutput>> ListCoins() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
/**
|
||||
* Find non-change parent output.
|
||||
*/
|
||||
const CTxOut& FindNonChangeParentOutput(const CTransaction& tx, int output) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
/**
|
||||
* Shuffle and select coins until nTargetValue is reached while avoiding
|
||||
* small change; This method is stochastic for some inputs and upon
|
||||
* completion the coin set and corresponding actual target value is
|
||||
* assembled
|
||||
* param@[in] coins Set of UTXOs to consider. These will be categorized into
|
||||
* OutputGroups and filtered using eligibility_filter before
|
||||
* selecting coins.
|
||||
* param@[out] setCoinsRet Populated with the coins selected if successful.
|
||||
* param@[out] nValueRet Used to return the total value of selected coins.
|
||||
*/
|
||||
bool AttemptSelection(const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector<COutput> coins,
|
||||
std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet, const CoinSelectionParams& coin_selection_params) const;
|
||||
|
||||
bool IsSpent(const uint256& hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
// Whether this or any known UTXO with the same single key has been spent.
|
||||
bool IsSpentKey(const uint256& hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void SetSpentKeyState(WalletBatch& batch, const uint256& hash, unsigned int n, bool used, std::set<CTxDestination>& tx_destinations) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
std::vector<OutputGroup> GroupOutputs(const std::vector<COutput>& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only) const;
|
||||
|
||||
/** Display address on an external signer. Returns false if external signer support is not compiled */
|
||||
bool DisplayAddress(const CTxDestination& dest) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
bool IsLockedCoin(uint256 hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void LockCoin(const COutPoint& output) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void UnlockCoin(const COutPoint& output) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void UnlockAllCoins() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void ListLockedCoins(std::vector<COutPoint>& vOutpts) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
/*
|
||||
* Rescan abort properties
|
||||
*/
|
||||
void AbortRescan() { fAbortRescan = true; }
|
||||
bool IsAbortingRescan() const { return fAbortRescan; }
|
||||
bool IsScanning() const { return fScanningWallet; }
|
||||
int64_t ScanningDuration() const { return fScanningWallet ? GetTimeMillis() - m_scanning_start : 0; }
|
||||
double ScanningProgress() const { return fScanningWallet ? (double) m_scanning_progress : 0; }
|
||||
|
||||
//! Upgrade stored CKeyMetadata objects to store key origin info as KeyOriginInfo
|
||||
void UpgradeKeyMetadata() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
//! Upgrade DescriptorCaches
|
||||
void UpgradeDescriptorCache() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
bool LoadMinVersion(int nVersion) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); nWalletVersion = nVersion; return true; }
|
||||
|
||||
//! Adds a destination data tuple to the store, without saving it to disk
|
||||
void LoadDestData(const CTxDestination& dest, const std::string& key, const std::string& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
//! Holds a timestamp at which point the wallet is scheduled (externally) to be relocked. Caller must arrange for actual relocking to occur via Lock().
|
||||
int64_t nRelockTime GUARDED_BY(cs_wallet){0};
|
||||
|
||||
// Used to prevent concurrent calls to walletpassphrase RPC.
|
||||
Mutex m_unlock_mutex;
|
||||
bool Unlock(const SecureString& strWalletPassphrase, bool accept_no_keys = false);
|
||||
bool ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, const SecureString& strNewWalletPassphrase);
|
||||
bool EncryptWallet(const SecureString& strWalletPassphrase);
|
||||
|
||||
void GetKeyBirthTimes(std::map<CKeyID, int64_t> &mapKeyBirth) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
unsigned int ComputeTimeSmart(const CWalletTx& wtx) const;
|
||||
|
||||
/**
|
||||
* Increment the next transaction order id
|
||||
* @return next transaction order id
|
||||
*/
|
||||
int64_t IncOrderPosNext(WalletBatch *batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
DBErrors ReorderTransactions();
|
||||
|
||||
void MarkDirty();
|
||||
|
||||
//! Callback for updating transaction metadata in mapWallet.
|
||||
//!
|
||||
//! @param wtx - reference to mapWallet transaction to update
|
||||
//! @param new_tx - true if wtx is newly inserted, false if it previously existed
|
||||
//!
|
||||
//! @return true if wtx is changed and needs to be saved to disk, otherwise false
|
||||
using UpdateWalletTxFn = std::function<bool(CWalletTx& wtx, bool new_tx)>;
|
||||
|
||||
CWalletTx* AddToWallet(CTransactionRef tx, const CWalletTx::Confirmation& confirm, const UpdateWalletTxFn& update_wtx=nullptr, bool fFlushOnClose=true);
|
||||
bool LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void transactionAddedToMempool(const CTransactionRef& tx, uint64_t mempool_sequence) override;
|
||||
void blockConnected(const CBlock& block, int height) override;
|
||||
void blockDisconnected(const CBlock& block, int height) override;
|
||||
void updatedBlockTip() override;
|
||||
int64_t RescanFromTime(int64_t startTime, const WalletRescanReserver& reserver, bool update);
|
||||
|
||||
struct ScanResult {
|
||||
enum { SUCCESS, FAILURE, USER_ABORT } status = SUCCESS;
|
||||
|
||||
//! Hash and height of most recent block that was successfully scanned.
|
||||
//! Unset if no blocks were scanned due to read errors or the chain
|
||||
//! being empty.
|
||||
uint256 last_scanned_block;
|
||||
std::optional<int> last_scanned_height;
|
||||
|
||||
//! Height of the most recent block that could not be scanned due to
|
||||
//! read errors or pruning. Will be set if status is FAILURE, unset if
|
||||
//! status is SUCCESS, and may or may not be set if status is
|
||||
//! USER_ABORT.
|
||||
uint256 last_failed_block;
|
||||
};
|
||||
ScanResult ScanForWalletTransactions(const uint256& start_block, int start_height, std::optional<int> max_height, const WalletRescanReserver& reserver, bool fUpdate);
|
||||
void transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason, uint64_t mempool_sequence) override;
|
||||
void ReacceptWalletTransactions() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void ResendWalletTransactions();
|
||||
struct Balance {
|
||||
CAmount m_mine_trusted{0}; //!< Trusted, at depth=GetBalance.min_depth or more
|
||||
CAmount m_mine_untrusted_pending{0}; //!< Untrusted, but in mempool (pending)
|
||||
CAmount m_mine_immature{0}; //!< Immature coinbases in the main chain
|
||||
CAmount m_watchonly_trusted{0};
|
||||
CAmount m_watchonly_untrusted_pending{0};
|
||||
CAmount m_watchonly_immature{0};
|
||||
};
|
||||
Balance GetBalance(int min_depth = 0, bool avoid_reuse = true) const;
|
||||
CAmount GetAvailableBalance(const CCoinControl* coinControl = nullptr) const;
|
||||
|
||||
OutputType TransactionChangeType(const std::optional<OutputType>& change_type, const std::vector<CRecipient>& vecSend) const;
|
||||
|
||||
/**
|
||||
* Insert additional inputs into the transaction by
|
||||
* calling CreateTransaction();
|
||||
*/
|
||||
bool FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, int& nChangePosInOut, bilingual_str& error, bool lockUnspents, const std::set<int>& setSubtractFeeFromOutputs, CCoinControl);
|
||||
/** Fetch the inputs and sign with SIGHASH_ALL. */
|
||||
bool SignTransaction(CMutableTransaction& tx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
/** Sign the tx given the input coins and sighash. */
|
||||
bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const;
|
||||
SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const;
|
||||
|
||||
/**
|
||||
* Fills out a PSBT with information from the wallet. Fills in UTXOs if we have
|
||||
* them. Tries to sign if sign=true. Sets `complete` if the PSBT is now complete
|
||||
* (i.e. has all required signatures or signature-parts, and is ready to
|
||||
* finalize.) Sets `error` and returns false if something goes wrong.
|
||||
*
|
||||
* @param[in] psbtx PartiallySignedTransaction to fill in
|
||||
* @param[out] complete indicates whether the PSBT is now complete
|
||||
* @param[in] sighash_type the sighash type to use when signing (if PSBT does not specify)
|
||||
* @param[in] sign whether to sign or not
|
||||
* @param[in] bip32derivs whether to fill in bip32 derivation information if available
|
||||
* return error
|
||||
*/
|
||||
TransactionError FillPSBT(PartiallySignedTransaction& psbtx,
|
||||
bool& complete,
|
||||
int sighash_type = 1 /* SIGHASH_ALL */,
|
||||
bool sign = true,
|
||||
bool bip32derivs = true,
|
||||
size_t* n_signed = nullptr) const;
|
||||
|
||||
/**
|
||||
* Create a new transaction paying the recipients with a set of coins
|
||||
* selected by SelectCoins(); Also create the change output, when needed
|
||||
* @note passing nChangePosInOut as -1 will result in setting a random position
|
||||
*/
|
||||
bool CreateTransaction(const std::vector<CRecipient>& vecSend, CTransactionRef& tx, CAmount& nFeeRet, int& nChangePosInOut, bilingual_str& error, const CCoinControl& coin_control, FeeCalculation& fee_calc_out, bool sign = true);
|
||||
/**
|
||||
* Submit the transaction to the node's mempool and then relay to peers.
|
||||
* Should be called after CreateTransaction unless you want to abort
|
||||
* broadcasting the transaction.
|
||||
*
|
||||
* @param[in] tx The transaction to be broadcast.
|
||||
* @param[in] mapValue key-values to be set on the transaction.
|
||||
* @param[in] orderForm BIP 70 / BIP 21 order form details to be set on the transaction.
|
||||
*/
|
||||
void CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::vector<std::pair<std::string, std::string>> orderForm);
|
||||
|
||||
bool DummySignTx(CMutableTransaction &txNew, const std::set<CTxOut> &txouts, bool use_max_sig = false) const
|
||||
{
|
||||
std::vector<CTxOut> v_txouts(txouts.size());
|
||||
std::copy(txouts.begin(), txouts.end(), v_txouts.begin());
|
||||
return DummySignTx(txNew, v_txouts, use_max_sig);
|
||||
}
|
||||
bool DummySignTx(CMutableTransaction &txNew, const std::vector<CTxOut> &txouts, bool use_max_sig = false) const;
|
||||
bool DummySignInput(CTxIn &tx_in, const CTxOut &txout, bool use_max_sig = false) const;
|
||||
|
||||
bool ImportScripts(const std::set<CScript> scripts, int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
bool ImportPrivKeys(const std::map<CKeyID, CKey>& privkey_map, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
bool ImportPubKeys(const std::vector<CKeyID>& ordered_pubkeys, const std::map<CKeyID, CPubKey>& pubkey_map, const std::map<CKeyID, std::pair<CPubKey, KeyOriginInfo>>& key_origins, const bool add_keypool, const bool internal, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
bool ImportScriptPubKeys(const std::string& label, const std::set<CScript>& script_pub_keys, const bool have_solving_data, const bool apply_label, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
CFeeRate m_pay_tx_fee{DEFAULT_PAY_TX_FEE};
|
||||
unsigned int m_confirm_target{DEFAULT_TX_CONFIRM_TARGET};
|
||||
/** Allow Coin Selection to pick unconfirmed UTXOs that were sent from our own wallet if it
|
||||
* cannot fund the transaction otherwise. */
|
||||
bool m_spend_zero_conf_change{DEFAULT_SPEND_ZEROCONF_CHANGE};
|
||||
bool m_signal_rbf{DEFAULT_WALLET_RBF};
|
||||
bool m_allow_fallback_fee{true}; //!< will be false if -fallbackfee=0
|
||||
CFeeRate m_min_fee{DEFAULT_TRANSACTION_MINFEE}; //!< Override with -mintxfee
|
||||
/**
|
||||
* If fee estimation does not have enough data to provide estimates, use this fee instead.
|
||||
* Has no effect if not using fee estimation
|
||||
* Override with -fallbackfee
|
||||
*/
|
||||
CFeeRate m_fallback_fee{DEFAULT_FALLBACK_FEE};
|
||||
|
||||
/** If the cost to spend a change output at this feerate is greater than the value of the
|
||||
* output itself, just drop it to fees. */
|
||||
CFeeRate m_discard_rate{DEFAULT_DISCARD_FEE};
|
||||
|
||||
/** The maximum fee amount we're willing to pay to prioritize partial spend avoidance. */
|
||||
CAmount m_max_aps_fee{DEFAULT_MAX_AVOIDPARTIALSPEND_FEE}; //!< note: this is absolute fee, not fee rate
|
||||
OutputType m_default_address_type{DEFAULT_ADDRESS_TYPE};
|
||||
/**
|
||||
* Default output type for change outputs. When unset, automatically choose type
|
||||
* based on address type setting and the types other of non-change outputs
|
||||
* (see -changetype option documentation and implementation in
|
||||
* CWallet::TransactionChangeType for details).
|
||||
*/
|
||||
std::optional<OutputType> m_default_change_type{};
|
||||
/** Absolute maximum transaction fee (in satoshis) used by default for the wallet */
|
||||
CAmount m_default_max_tx_fee{DEFAULT_TRANSACTION_MAXFEE};
|
||||
|
||||
size_t KeypoolCountExternalKeys() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
bool TopUpKeyPool(unsigned int kpSize = 0);
|
||||
|
||||
int64_t GetOldestKeyPoolTime() const;
|
||||
|
||||
std::set<std::set<CTxDestination>> GetAddressGroupings() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
std::map<CTxDestination, CAmount> GetAddressBalances() const;
|
||||
|
||||
std::set<CTxDestination> GetLabelAddresses(const std::string& label) const;
|
||||
|
||||
/**
|
||||
* Marks all outputs in each one of the destinations dirty, so their cache is
|
||||
* reset and does not return outdated information.
|
||||
*/
|
||||
void MarkDestinationsDirty(const std::set<CTxDestination>& destinations) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
bool GetNewDestination(const OutputType type, const std::string label, CTxDestination& dest, std::string& error);
|
||||
bool GetNewChangeDestination(const OutputType type, CTxDestination& dest, std::string& error);
|
||||
|
||||
isminetype IsMine(const CTxDestination& dest) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
isminetype IsMine(const CScript& script) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
isminetype IsMine(const CTxIn& txin) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
/**
|
||||
* Returns amount of debit if the input matches the
|
||||
* filter, otherwise returns 0
|
||||
*/
|
||||
CAmount GetDebit(const CTxIn& txin, const isminefilter& filter) const;
|
||||
isminetype IsMine(const CTxOut& txout) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
CAmount GetCredit(const CTxOut& txout, const isminefilter& filter) const;
|
||||
bool IsChange(const CTxOut& txout) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
bool IsChange(const CScript& script) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
CAmount GetChange(const CTxOut& txout) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
bool IsMine(const CTransaction& tx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
/** should probably be renamed to IsRelevantToMe */
|
||||
bool IsFromMe(const CTransaction& tx) const;
|
||||
CAmount GetDebit(const CTransaction& tx, const isminefilter& filter) const;
|
||||
/** Returns whether all of the inputs match the filter */
|
||||
bool IsAllFromMe(const CTransaction& tx, const isminefilter& filter) const;
|
||||
CAmount GetCredit(const CTransaction& tx, const isminefilter& filter) const;
|
||||
CAmount GetChange(const CTransaction& tx) const;
|
||||
void chainStateFlushed(const CBlockLocator& loc) override;
|
||||
|
||||
DBErrors LoadWallet();
|
||||
DBErrors ZapSelectTx(std::vector<uint256>& vHashIn, std::vector<uint256>& vHashOut) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
bool SetAddressBook(const CTxDestination& address, const std::string& strName, const std::string& purpose);
|
||||
|
||||
bool DelAddressBook(const CTxDestination& address);
|
||||
|
||||
bool IsAddressUsed(const CTxDestination& dest) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
bool SetAddressUsed(WalletBatch& batch, const CTxDestination& dest, bool used) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
std::vector<std::string> GetAddressReceiveRequests() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
bool SetAddressReceiveRequest(WalletBatch& batch, const CTxDestination& dest, const std::string& id, const std::string& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
unsigned int GetKeyPoolSize() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
//! signify that a particular wallet feature is now used.
|
||||
void SetMinVersion(enum WalletFeature, WalletBatch* batch_in = nullptr) override;
|
||||
|
||||
//! get the current wallet format (the oldest client version guaranteed to understand this wallet)
|
||||
int GetVersion() const { LOCK(cs_wallet); return nWalletVersion; }
|
||||
|
||||
//! Get wallet transactions that conflict with given transaction (spend same outputs)
|
||||
std::set<uint256> GetConflicts(const uint256& txid) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
//! Check if a given transaction has any of its outputs spent by another transaction in the wallet
|
||||
bool HasWalletSpend(const uint256& txid) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
//! Flush wallet (bitdb flush)
|
||||
void Flush();
|
||||
|
||||
//! Close wallet database
|
||||
void Close();
|
||||
|
||||
/** Wallet is about to be unloaded */
|
||||
boost::signals2::signal<void ()> NotifyUnload;
|
||||
|
||||
/**
|
||||
* Address book entry changed.
|
||||
* @note called without lock cs_wallet held.
|
||||
*/
|
||||
boost::signals2::signal<void(const CTxDestination& address,
|
||||
const std::string& label, bool isMine,
|
||||
const std::string& purpose, ChangeType status)>
|
||||
NotifyAddressBookChanged;
|
||||
|
||||
/**
|
||||
* Wallet transaction added, removed or updated.
|
||||
* @note called with lock cs_wallet held.
|
||||
*/
|
||||
boost::signals2::signal<void(const uint256& hashTx, ChangeType status)> NotifyTransactionChanged;
|
||||
|
||||
/** Show progress e.g. for rescan */
|
||||
boost::signals2::signal<void (const std::string &title, int nProgress)> ShowProgress;
|
||||
|
||||
/** Watch-only address added */
|
||||
boost::signals2::signal<void (bool fHaveWatchOnly)> NotifyWatchonlyChanged;
|
||||
|
||||
/** Keypool has new keys */
|
||||
boost::signals2::signal<void ()> NotifyCanGetAddressesChanged;
|
||||
|
||||
/**
|
||||
* Wallet status (encrypted, locked) changed.
|
||||
* Note: Called without locks held.
|
||||
*/
|
||||
boost::signals2::signal<void (CWallet* wallet)> NotifyStatusChanged;
|
||||
|
||||
/** Inquire whether this wallet broadcasts transactions. */
|
||||
bool GetBroadcastTransactions() const { return fBroadcastTransactions; }
|
||||
/** Set whether this wallet broadcasts transactions. */
|
||||
void SetBroadcastTransactions(bool broadcast) { fBroadcastTransactions = broadcast; }
|
||||
|
||||
/** Return whether transaction can be abandoned */
|
||||
bool TransactionCanBeAbandoned(const uint256& hashTx) const;
|
||||
|
||||
/* Mark a transaction (and it in-wallet descendants) as abandoned so its inputs may be respent. */
|
||||
bool AbandonTransaction(const uint256& hashTx);
|
||||
|
||||
/** Mark a transaction as replaced by another transaction (e.g., BIP 125). */
|
||||
bool MarkReplaced(const uint256& originalHash, const uint256& newHash);
|
||||
|
||||
/* Initializes the wallet, returns a new CWallet instance or a null pointer in case of an error */
|
||||
static std::shared_ptr<CWallet> Create(interfaces::Chain* chain, const std::string& name, std::unique_ptr<WalletDatabase> database, uint64_t wallet_creation_flags, bilingual_str& error, std::vector<bilingual_str>& warnings);
|
||||
|
||||
/**
|
||||
* Wallet post-init setup
|
||||
* Gives the wallet a chance to register repetitive tasks and complete post-init tasks
|
||||
*/
|
||||
void postInitProcess();
|
||||
|
||||
bool BackupWallet(const std::string& strDest) const;
|
||||
|
||||
/* Returns true if HD is enabled */
|
||||
bool IsHDEnabled() const;
|
||||
|
||||
/* Returns true if the wallet can give out new addresses. This means it has keys in the keypool or can generate new keys */
|
||||
bool CanGetAddresses(bool internal = false) const;
|
||||
|
||||
/**
|
||||
* Blocks until the wallet state is up-to-date to /at least/ the current
|
||||
* chain at the time this function is entered
|
||||
* Obviously holding cs_main/cs_wallet when going into this call may cause
|
||||
* deadlock
|
||||
*/
|
||||
void BlockUntilSyncedToCurrentChain() const LOCKS_EXCLUDED(::cs_main) EXCLUSIVE_LOCKS_REQUIRED(!cs_wallet);
|
||||
|
||||
/** set a single wallet flag */
|
||||
void SetWalletFlag(uint64_t flags);
|
||||
|
||||
/** Unsets a single wallet flag */
|
||||
void UnsetWalletFlag(uint64_t flag);
|
||||
|
||||
/** check if a certain wallet flag is set */
|
||||
bool IsWalletFlagSet(uint64_t flag) const override;
|
||||
|
||||
/** overwrite all flags by the given uint64_t
|
||||
returns false if unknown, non-tolerable flags are present */
|
||||
bool AddWalletFlags(uint64_t flags);
|
||||
/** Loads the flags into the wallet. (used by LoadWallet) */
|
||||
bool LoadWalletFlags(uint64_t flags);
|
||||
|
||||
/** Determine if we are a legacy wallet */
|
||||
bool IsLegacy() const;
|
||||
|
||||
/** Returns a bracketed wallet name for displaying in logs, will return [default wallet] if the wallet has no name */
|
||||
const std::string GetDisplayName() const override {
|
||||
std::string wallet_name = GetName().length() == 0 ? "default wallet" : GetName();
|
||||
return strprintf("[%s]", wallet_name);
|
||||
};
|
||||
|
||||
/** Prepends the wallet name in logging output to ease debugging in multi-wallet use cases */
|
||||
template<typename... Params>
|
||||
void WalletLogPrintf(std::string fmt, Params... parameters) const {
|
||||
LogPrintf(("%s " + fmt).c_str(), GetDisplayName(), parameters...);
|
||||
};
|
||||
|
||||
/** Upgrade the wallet */
|
||||
bool UpgradeWallet(int version, bilingual_str& error);
|
||||
|
||||
//! Returns all unique ScriptPubKeyMans in m_internal_spk_managers and m_external_spk_managers
|
||||
std::set<ScriptPubKeyMan*> GetActiveScriptPubKeyMans() const;
|
||||
|
||||
//! Returns all unique ScriptPubKeyMans
|
||||
std::set<ScriptPubKeyMan*> GetAllScriptPubKeyMans() const;
|
||||
|
||||
//! Get the ScriptPubKeyMan for the given OutputType and internal/external chain.
|
||||
ScriptPubKeyMan* GetScriptPubKeyMan(const OutputType& type, bool internal) const;
|
||||
|
||||
//! Get the ScriptPubKeyMan for a script
|
||||
ScriptPubKeyMan* GetScriptPubKeyMan(const CScript& script) const;
|
||||
//! Get the ScriptPubKeyMan by id
|
||||
ScriptPubKeyMan* GetScriptPubKeyMan(const uint256& id) const;
|
||||
|
||||
//! Get all of the ScriptPubKeyMans for a script given additional information in sigdata (populated by e.g. a psbt)
|
||||
std::set<ScriptPubKeyMan*> GetScriptPubKeyMans(const CScript& script, SignatureData& sigdata) const;
|
||||
|
||||
//! Get the SigningProvider for a script
|
||||
std::unique_ptr<SigningProvider> GetSolvingProvider(const CScript& script) const;
|
||||
std::unique_ptr<SigningProvider> GetSolvingProvider(const CScript& script, SignatureData& sigdata) const;
|
||||
|
||||
//! Get the LegacyScriptPubKeyMan which is used for all types, internal, and external.
|
||||
LegacyScriptPubKeyMan* GetLegacyScriptPubKeyMan() const;
|
||||
LegacyScriptPubKeyMan* GetOrCreateLegacyScriptPubKeyMan();
|
||||
|
||||
//! Make a LegacyScriptPubKeyMan and set it for all types, internal, and external.
|
||||
void SetupLegacyScriptPubKeyMan();
|
||||
|
||||
const CKeyingMaterial& GetEncryptionKey() const override;
|
||||
bool HasEncryptionKeys() const override;
|
||||
|
||||
/** Get last block processed height */
|
||||
int GetLastBlockHeight() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet)
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
assert(m_last_block_processed_height >= 0);
|
||||
return m_last_block_processed_height;
|
||||
};
|
||||
uint256 GetLastBlockHash() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet)
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
assert(m_last_block_processed_height >= 0);
|
||||
return m_last_block_processed;
|
||||
}
|
||||
/** Set last block processed height, currently only use in unit test */
|
||||
void SetLastBlockProcessed(int block_height, uint256 block_hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet)
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
m_last_block_processed_height = block_height;
|
||||
m_last_block_processed = block_hash;
|
||||
};
|
||||
|
||||
//! Connect the signals from ScriptPubKeyMans to the signals in CWallet
|
||||
void ConnectScriptPubKeyManNotifiers();
|
||||
|
||||
//! Instantiate a descriptor ScriptPubKeyMan from the WalletDescriptor and load it
|
||||
void LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc);
|
||||
|
||||
//! Adds the active ScriptPubKeyMan for the specified type and internal. Writes it to the wallet file
|
||||
//! @param[in] id The unique id for the ScriptPubKeyMan
|
||||
//! @param[in] type The OutputType this ScriptPubKeyMan provides addresses for
|
||||
//! @param[in] internal Whether this ScriptPubKeyMan provides change addresses
|
||||
void AddActiveScriptPubKeyMan(uint256 id, OutputType type, bool internal);
|
||||
|
||||
//! Loads an active ScriptPubKeyMan for the specified type and internal. (used by LoadWallet)
|
||||
//! @param[in] id The unique id for the ScriptPubKeyMan
|
||||
//! @param[in] type The OutputType this ScriptPubKeyMan provides addresses for
|
||||
//! @param[in] internal Whether this ScriptPubKeyMan provides change addresses
|
||||
void LoadActiveScriptPubKeyMan(uint256 id, OutputType type, bool internal);
|
||||
|
||||
//! Remove specified ScriptPubKeyMan from set of active SPK managers. Writes the change to the wallet file.
|
||||
//! @param[in] id The unique id for the ScriptPubKeyMan
|
||||
//! @param[in] type The OutputType this ScriptPubKeyMan provides addresses for
|
||||
//! @param[in] internal Whether this ScriptPubKeyMan provides change addresses
|
||||
void DeactivateScriptPubKeyMan(uint256 id, OutputType type, bool internal);
|
||||
|
||||
//! Create new DescriptorScriptPubKeyMans and add them to the wallet
|
||||
void SetupDescriptorScriptPubKeyMans() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
//! Return the DescriptorScriptPubKeyMan for a WalletDescriptor if it is already in the wallet
|
||||
DescriptorScriptPubKeyMan* GetDescriptorScriptPubKeyMan(const WalletDescriptor& desc) const;
|
||||
|
||||
//! Add a descriptor to the wallet, return a ScriptPubKeyMan & associated output type
|
||||
ScriptPubKeyMan* AddWalletDescriptor(WalletDescriptor& desc, const FlatSigningProvider& signing_provider, const std::string& label, bool internal);
|
||||
};
|
||||
|
||||
/**
|
||||
* Called periodically by the schedule thread. Prompts individual wallets to resend
|
||||
* their transactions. Actual rebroadcast schedule is managed by the wallets themselves.
|
||||
*/
|
||||
void MaybeResendWalletTxs();
|
||||
|
||||
/** RAII object to check and reserve a wallet rescan */
|
||||
class WalletRescanReserver
|
||||
{
|
||||
private:
|
||||
CWallet& m_wallet;
|
||||
bool m_could_reserve;
|
||||
public:
|
||||
explicit WalletRescanReserver(CWallet& w) : m_wallet(w), m_could_reserve(false) {}
|
||||
|
||||
bool reserve()
|
||||
{
|
||||
assert(!m_could_reserve);
|
||||
if (m_wallet.fScanningWallet.exchange(true)) {
|
||||
return false;
|
||||
}
|
||||
m_wallet.m_scanning_start = GetTimeMillis();
|
||||
m_wallet.m_scanning_progress = 0;
|
||||
m_could_reserve = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isReserved() const
|
||||
{
|
||||
return (m_could_reserve && m_wallet.fScanningWallet);
|
||||
}
|
||||
|
||||
~WalletRescanReserver()
|
||||
{
|
||||
if (m_could_reserve) {
|
||||
m_wallet.fScanningWallet = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct TxSize {
|
||||
int64_t vsize{-1};
|
||||
int64_t weight{-1};
|
||||
};
|
||||
|
||||
/** Calculate the size of the transaction assuming all signatures are max size
|
||||
* Use DummySignatureCreator, which inserts 71 byte signatures everywhere.
|
||||
* NOTE: this requires that all inputs must be in mapWallet (eg the tx should
|
||||
* be IsAllFromMe). */
|
||||
TxSize CalculateMaximumSignedTxSize(const CTransaction& tx, const CWallet* wallet, bool use_max_sig = false) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet);
|
||||
TxSize CalculateMaximumSignedTxSize(const CTransaction& tx, const CWallet* wallet, const std::vector<CTxOut>& txouts, bool use_max_sig = false);
|
||||
|
||||
//! Add wallet name to persistent configuration so it will be loaded on startup.
|
||||
bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name);
|
||||
|
||||
//! Remove wallet name from persistent configuration so it will not be loaded on startup.
|
||||
bool RemoveWalletSetting(interfaces::Chain& chain, const std::string& wallet_name);
|
||||
|
||||
#endif // DIGIBYTE_WALLET_WALLET_H
|
||||
1165
src/wallet/walletdb.cpp
Normal file
1165
src/wallet/walletdb.cpp
Normal file
File diff suppressed because it is too large
Load diff
297
src/wallet/walletdb.h
Normal file
297
src/wallet/walletdb.h
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2014-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_WALLETDB_H
|
||||
#define DIGIBYTE_WALLET_WALLETDB_H
|
||||
|
||||
#include <amount.h>
|
||||
#include <script/sign.h>
|
||||
#include <wallet/db.h>
|
||||
#include <wallet/walletutil.h>
|
||||
#include <key.h>
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Overview of wallet database classes:
|
||||
*
|
||||
* - WalletBatch is an abstract modifier object for the wallet database, and encapsulates a database
|
||||
* batch update as well as methods to act on the database. It should be agnostic to the database implementation.
|
||||
*
|
||||
* The following classes are implementation specific:
|
||||
* - BerkeleyEnvironment is an environment in which the database exists.
|
||||
* - BerkeleyDatabase represents a wallet database.
|
||||
* - BerkeleyBatch is a low-level database batch update.
|
||||
*/
|
||||
|
||||
static const bool DEFAULT_FLUSHWALLET = true;
|
||||
|
||||
struct CBlockLocator;
|
||||
class CKeyPool;
|
||||
class CMasterKey;
|
||||
class CScript;
|
||||
class CWallet;
|
||||
class CWalletTx;
|
||||
class uint160;
|
||||
class uint256;
|
||||
|
||||
/** Error statuses for the wallet database */
|
||||
enum class DBErrors
|
||||
{
|
||||
LOAD_OK,
|
||||
CORRUPT,
|
||||
NONCRITICAL_ERROR,
|
||||
TOO_NEW,
|
||||
LOAD_FAIL,
|
||||
NEED_REWRITE
|
||||
};
|
||||
|
||||
namespace DBKeys {
|
||||
extern const std::string ACENTRY;
|
||||
extern const std::string ACTIVEEXTERNALSPK;
|
||||
extern const std::string ACTIVEINTERNALSPK;
|
||||
extern const std::string BESTBLOCK;
|
||||
extern const std::string BESTBLOCK_NOMERKLE;
|
||||
extern const std::string CRYPTED_KEY;
|
||||
extern const std::string CSCRIPT;
|
||||
extern const std::string DEFAULTKEY;
|
||||
extern const std::string DESTDATA;
|
||||
extern const std::string FLAGS;
|
||||
extern const std::string HDCHAIN;
|
||||
extern const std::string KEY;
|
||||
extern const std::string KEYMETA;
|
||||
extern const std::string MASTER_KEY;
|
||||
extern const std::string MINVERSION;
|
||||
extern const std::string NAME;
|
||||
extern const std::string OLD_KEY;
|
||||
extern const std::string ORDERPOSNEXT;
|
||||
extern const std::string POOL;
|
||||
extern const std::string PURPOSE;
|
||||
extern const std::string SETTINGS;
|
||||
extern const std::string TX;
|
||||
extern const std::string VERSION;
|
||||
extern const std::string WALLETDESCRIPTOR;
|
||||
extern const std::string WALLETDESCRIPTORCKEY;
|
||||
extern const std::string WALLETDESCRIPTORKEY;
|
||||
extern const std::string WATCHMETA;
|
||||
extern const std::string WATCHS;
|
||||
} // namespace DBKeys
|
||||
|
||||
/* simple HD chain data model */
|
||||
class CHDChain
|
||||
{
|
||||
public:
|
||||
uint32_t nExternalChainCounter;
|
||||
uint32_t nInternalChainCounter;
|
||||
CKeyID seed_id; //!< seed hash160
|
||||
|
||||
static const int VERSION_HD_BASE = 1;
|
||||
static const int VERSION_HD_CHAIN_SPLIT = 2;
|
||||
static const int CURRENT_VERSION = VERSION_HD_CHAIN_SPLIT;
|
||||
int nVersion;
|
||||
|
||||
CHDChain() { SetNull(); }
|
||||
|
||||
SERIALIZE_METHODS(CHDChain, obj)
|
||||
{
|
||||
READWRITE(obj.nVersion, obj.nExternalChainCounter, obj.seed_id);
|
||||
if (obj.nVersion >= VERSION_HD_CHAIN_SPLIT) {
|
||||
READWRITE(obj.nInternalChainCounter);
|
||||
}
|
||||
}
|
||||
|
||||
void SetNull()
|
||||
{
|
||||
nVersion = CHDChain::CURRENT_VERSION;
|
||||
nExternalChainCounter = 0;
|
||||
nInternalChainCounter = 0;
|
||||
seed_id.SetNull();
|
||||
}
|
||||
|
||||
bool operator==(const CHDChain& chain) const
|
||||
{
|
||||
return seed_id == chain.seed_id;
|
||||
}
|
||||
};
|
||||
|
||||
class CKeyMetadata
|
||||
{
|
||||
public:
|
||||
static const int VERSION_BASIC=1;
|
||||
static const int VERSION_WITH_HDDATA=10;
|
||||
static const int VERSION_WITH_KEY_ORIGIN = 12;
|
||||
static const int CURRENT_VERSION=VERSION_WITH_KEY_ORIGIN;
|
||||
int nVersion;
|
||||
int64_t nCreateTime; // 0 means unknown
|
||||
std::string hdKeypath; //optional HD/bip32 keypath. Still used to determine whether a key is a seed. Also kept for backwards compatibility
|
||||
CKeyID hd_seed_id; //id of the HD seed used to derive this key
|
||||
KeyOriginInfo key_origin; // Key origin info with path and fingerprint
|
||||
bool has_key_origin = false; //!< Whether the key_origin is useful
|
||||
|
||||
CKeyMetadata()
|
||||
{
|
||||
SetNull();
|
||||
}
|
||||
explicit CKeyMetadata(int64_t nCreateTime_)
|
||||
{
|
||||
SetNull();
|
||||
nCreateTime = nCreateTime_;
|
||||
}
|
||||
|
||||
SERIALIZE_METHODS(CKeyMetadata, obj)
|
||||
{
|
||||
READWRITE(obj.nVersion, obj.nCreateTime);
|
||||
if (obj.nVersion >= VERSION_WITH_HDDATA) {
|
||||
READWRITE(obj.hdKeypath, obj.hd_seed_id);
|
||||
}
|
||||
if (obj.nVersion >= VERSION_WITH_KEY_ORIGIN)
|
||||
{
|
||||
READWRITE(obj.key_origin);
|
||||
READWRITE(obj.has_key_origin);
|
||||
}
|
||||
}
|
||||
|
||||
void SetNull()
|
||||
{
|
||||
nVersion = CKeyMetadata::CURRENT_VERSION;
|
||||
nCreateTime = 0;
|
||||
hdKeypath.clear();
|
||||
hd_seed_id.SetNull();
|
||||
key_origin.clear();
|
||||
has_key_origin = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** Access to the wallet database.
|
||||
* Opens the database and provides read and write access to it. Each read and write is its own transaction.
|
||||
* Multiple operation transactions can be started using TxnBegin() and committed using TxnCommit()
|
||||
* Otherwise the transaction will be committed when the object goes out of scope.
|
||||
* Optionally (on by default) it will flush to disk on close.
|
||||
* Every 1000 writes will automatically trigger a flush to disk.
|
||||
*/
|
||||
class WalletBatch
|
||||
{
|
||||
private:
|
||||
template <typename K, typename T>
|
||||
bool WriteIC(const K& key, const T& value, bool fOverwrite = true)
|
||||
{
|
||||
if (!m_batch->Write(key, value, fOverwrite)) {
|
||||
return false;
|
||||
}
|
||||
m_database.IncrementUpdateCounter();
|
||||
if (m_database.nUpdateCounter % 1000 == 0) {
|
||||
m_batch->Flush();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename K>
|
||||
bool EraseIC(const K& key)
|
||||
{
|
||||
if (!m_batch->Erase(key)) {
|
||||
return false;
|
||||
}
|
||||
m_database.IncrementUpdateCounter();
|
||||
if (m_database.nUpdateCounter % 1000 == 0) {
|
||||
m_batch->Flush();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public:
|
||||
explicit WalletBatch(WalletDatabase &database, bool _fFlushOnClose = true) :
|
||||
m_batch(database.MakeBatch(_fFlushOnClose)),
|
||||
m_database(database)
|
||||
{
|
||||
}
|
||||
WalletBatch(const WalletBatch&) = delete;
|
||||
WalletBatch& operator=(const WalletBatch&) = delete;
|
||||
|
||||
bool WriteName(const std::string& strAddress, const std::string& strName);
|
||||
bool EraseName(const std::string& strAddress);
|
||||
|
||||
bool WritePurpose(const std::string& strAddress, const std::string& purpose);
|
||||
bool ErasePurpose(const std::string& strAddress);
|
||||
|
||||
bool WriteTx(const CWalletTx& wtx);
|
||||
bool EraseTx(uint256 hash);
|
||||
|
||||
bool WriteKeyMetadata(const CKeyMetadata& meta, const CPubKey& pubkey, const bool overwrite);
|
||||
bool WriteKey(const CPubKey& vchPubKey, const CPrivKey& vchPrivKey, const CKeyMetadata &keyMeta);
|
||||
bool WriteCryptedKey(const CPubKey& vchPubKey, const std::vector<unsigned char>& vchCryptedSecret, const CKeyMetadata &keyMeta);
|
||||
bool WriteMasterKey(unsigned int nID, const CMasterKey& kMasterKey);
|
||||
|
||||
bool WriteCScript(const uint160& hash, const CScript& redeemScript);
|
||||
|
||||
bool WriteWatchOnly(const CScript &script, const CKeyMetadata &keymeta);
|
||||
bool EraseWatchOnly(const CScript &script);
|
||||
|
||||
bool WriteBestBlock(const CBlockLocator& locator);
|
||||
bool ReadBestBlock(CBlockLocator& locator);
|
||||
|
||||
bool WriteOrderPosNext(int64_t nOrderPosNext);
|
||||
|
||||
bool ReadPool(int64_t nPool, CKeyPool& keypool);
|
||||
bool WritePool(int64_t nPool, const CKeyPool& keypool);
|
||||
bool ErasePool(int64_t nPool);
|
||||
|
||||
bool WriteMinVersion(int nVersion);
|
||||
|
||||
bool WriteDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const CPrivKey& privkey);
|
||||
bool WriteCryptedDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const std::vector<unsigned char>& secret);
|
||||
bool WriteDescriptor(const uint256& desc_id, const WalletDescriptor& descriptor);
|
||||
bool WriteDescriptorDerivedCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index, uint32_t der_index);
|
||||
bool WriteDescriptorParentCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index);
|
||||
bool WriteDescriptorLastHardenedCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index);
|
||||
bool WriteDescriptorCacheItems(const uint256& desc_id, const DescriptorCache& cache);
|
||||
|
||||
/// Write destination data key,value tuple to database
|
||||
bool WriteDestData(const std::string &address, const std::string &key, const std::string &value);
|
||||
/// Erase destination data tuple from wallet database
|
||||
bool EraseDestData(const std::string &address, const std::string &key);
|
||||
|
||||
bool WriteActiveScriptPubKeyMan(uint8_t type, const uint256& id, bool internal);
|
||||
bool EraseActiveScriptPubKeyMan(uint8_t type, bool internal);
|
||||
|
||||
DBErrors LoadWallet(CWallet* pwallet);
|
||||
DBErrors FindWalletTx(std::vector<uint256>& vTxHash, std::list<CWalletTx>& vWtx);
|
||||
DBErrors ZapSelectTx(std::vector<uint256>& vHashIn, std::vector<uint256>& vHashOut);
|
||||
/* Function to determine if a certain KV/key-type is a key (cryptographical key) type */
|
||||
static bool IsKeyType(const std::string& strType);
|
||||
|
||||
//! write the hdchain model (external chain child index counter)
|
||||
bool WriteHDChain(const CHDChain& chain);
|
||||
|
||||
bool WriteWalletFlags(const uint64_t flags);
|
||||
//! Begin a new transaction
|
||||
bool TxnBegin();
|
||||
//! Commit current transaction
|
||||
bool TxnCommit();
|
||||
//! Abort current transaction
|
||||
bool TxnAbort();
|
||||
private:
|
||||
std::unique_ptr<DatabaseBatch> m_batch;
|
||||
WalletDatabase& m_database;
|
||||
};
|
||||
|
||||
//! Compacts BDB state so that wallet.dat is self-contained (if there are changes)
|
||||
void MaybeCompactWalletDB();
|
||||
|
||||
//! Callback for filtering key types to deserialize in ReadKeyValue
|
||||
using KeyFilterFn = std::function<bool(const std::string&)>;
|
||||
|
||||
//! Unserialize a given Key-Value pair and load it into the wallet
|
||||
bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, std::string& strType, std::string& strErr, const KeyFilterFn& filter_fn = nullptr);
|
||||
|
||||
/** Return object for accessing dummy database with no read/write capabilities. */
|
||||
std::unique_ptr<WalletDatabase> CreateDummyWalletDatabase();
|
||||
|
||||
/** Return object for accessing temporary in-memory database. */
|
||||
std::unique_ptr<WalletDatabase> CreateMockWalletDatabase();
|
||||
|
||||
#endif // DIGIBYTE_WALLET_WALLETDB_H
|
||||
211
src/wallet/wallettool.cpp
Normal file
211
src/wallet/wallettool.cpp
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
// Copyright (c) 2016-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <fs.h>
|
||||
#include <util/system.h>
|
||||
#include <util/translation.h>
|
||||
#include <wallet/dump.h>
|
||||
#include <wallet/salvage.h>
|
||||
#include <wallet/wallet.h>
|
||||
#include <wallet/walletutil.h>
|
||||
|
||||
namespace WalletTool {
|
||||
|
||||
// The standard wallet deleter function blocks on the validation interface
|
||||
// queue, which doesn't exist for the digibyte-wallet. Define our own
|
||||
// deleter here.
|
||||
static void WalletToolReleaseWallet(CWallet* wallet)
|
||||
{
|
||||
wallet->WalletLogPrintf("Releasing wallet\n");
|
||||
wallet->Close();
|
||||
delete wallet;
|
||||
}
|
||||
|
||||
static void WalletCreate(CWallet* wallet_instance, uint64_t wallet_creation_flags)
|
||||
{
|
||||
LOCK(wallet_instance->cs_wallet);
|
||||
|
||||
wallet_instance->SetMinVersion(FEATURE_HD_SPLIT);
|
||||
wallet_instance->AddWalletFlags(wallet_creation_flags);
|
||||
|
||||
if (!wallet_instance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
|
||||
auto spk_man = wallet_instance->GetOrCreateLegacyScriptPubKeyMan();
|
||||
spk_man->SetupGeneration(false);
|
||||
} else {
|
||||
wallet_instance->SetupDescriptorScriptPubKeyMans();
|
||||
}
|
||||
|
||||
tfm::format(std::cout, "Topping up keypool...\n");
|
||||
wallet_instance->TopUpKeyPool();
|
||||
}
|
||||
|
||||
static std::shared_ptr<CWallet> MakeWallet(const std::string& name, const fs::path& path, DatabaseOptions options)
|
||||
{
|
||||
DatabaseStatus status;
|
||||
bilingual_str error;
|
||||
std::unique_ptr<WalletDatabase> database = MakeDatabase(path, options, status, error);
|
||||
if (!database) {
|
||||
tfm::format(std::cerr, "%s\n", error.original);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// dummy chain interface
|
||||
std::shared_ptr<CWallet> wallet_instance{new CWallet(nullptr /* chain */, name, std::move(database)), WalletToolReleaseWallet};
|
||||
DBErrors load_wallet_ret;
|
||||
try {
|
||||
load_wallet_ret = wallet_instance->LoadWallet();
|
||||
} catch (const std::runtime_error&) {
|
||||
tfm::format(std::cerr, "Error loading %s. Is wallet being used by another process?\n", name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (load_wallet_ret != DBErrors::LOAD_OK) {
|
||||
wallet_instance = nullptr;
|
||||
if (load_wallet_ret == DBErrors::CORRUPT) {
|
||||
tfm::format(std::cerr, "Error loading %s: Wallet corrupted", name);
|
||||
return nullptr;
|
||||
} else if (load_wallet_ret == DBErrors::NONCRITICAL_ERROR) {
|
||||
tfm::format(std::cerr, "Error reading %s! All keys read correctly, but transaction data"
|
||||
" or address book entries might be missing or incorrect.",
|
||||
name);
|
||||
} else if (load_wallet_ret == DBErrors::TOO_NEW) {
|
||||
tfm::format(std::cerr, "Error loading %s: Wallet requires newer version of %s",
|
||||
name, PACKAGE_NAME);
|
||||
return nullptr;
|
||||
} else if (load_wallet_ret == DBErrors::NEED_REWRITE) {
|
||||
tfm::format(std::cerr, "Wallet needed to be rewritten: restart %s to complete", PACKAGE_NAME);
|
||||
return nullptr;
|
||||
} else {
|
||||
tfm::format(std::cerr, "Error loading %s", name);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.require_create) WalletCreate(wallet_instance.get(), options.create_flags);
|
||||
|
||||
return wallet_instance;
|
||||
}
|
||||
|
||||
static void WalletShowInfo(CWallet* wallet_instance)
|
||||
{
|
||||
LOCK(wallet_instance->cs_wallet);
|
||||
|
||||
tfm::format(std::cout, "Wallet info\n===========\n");
|
||||
tfm::format(std::cout, "Name: %s\n", wallet_instance->GetName());
|
||||
tfm::format(std::cout, "Format: %s\n", wallet_instance->GetDatabase().Format());
|
||||
tfm::format(std::cout, "Descriptors: %s\n", wallet_instance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS) ? "yes" : "no");
|
||||
tfm::format(std::cout, "Encrypted: %s\n", wallet_instance->IsCrypted() ? "yes" : "no");
|
||||
tfm::format(std::cout, "HD (hd seed available): %s\n", wallet_instance->IsHDEnabled() ? "yes" : "no");
|
||||
tfm::format(std::cout, "Keypool Size: %u\n", wallet_instance->GetKeyPoolSize());
|
||||
tfm::format(std::cout, "Transactions: %zu\n", wallet_instance->mapWallet.size());
|
||||
tfm::format(std::cout, "Address Book: %zu\n", wallet_instance->m_address_book.size());
|
||||
}
|
||||
|
||||
bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command)
|
||||
{
|
||||
if (args.IsArgSet("-format") && command != "createfromdump") {
|
||||
tfm::format(std::cerr, "The -format option can only be used with the \"createfromdump\" command.\n");
|
||||
return false;
|
||||
}
|
||||
if (args.IsArgSet("-dumpfile") && command != "dump" && command != "createfromdump") {
|
||||
tfm::format(std::cerr, "The -dumpfile option can only be used with the \"dump\" and \"createfromdump\" commands.\n");
|
||||
return false;
|
||||
}
|
||||
if (args.IsArgSet("-descriptors") && command != "create") {
|
||||
tfm::format(std::cerr, "The -descriptors option can only be used with the 'create' command.\n");
|
||||
return false;
|
||||
}
|
||||
if (args.IsArgSet("-legacy") && command != "create") {
|
||||
tfm::format(std::cerr, "The -legacy option can only be used with the 'create' command.\n");
|
||||
return false;
|
||||
}
|
||||
if (command == "create" && !args.IsArgSet("-wallet")) {
|
||||
tfm::format(std::cerr, "Wallet name must be provided when creating a new wallet.\n");
|
||||
return false;
|
||||
}
|
||||
const std::string name = args.GetArg("-wallet", "");
|
||||
const fs::path path = fsbridge::AbsPathJoin(GetWalletDir(), name);
|
||||
|
||||
if (command == "create") {
|
||||
DatabaseOptions options;
|
||||
options.require_create = true;
|
||||
// If -legacy is set, use it. Otherwise default to false.
|
||||
bool make_legacy = args.GetBoolArg("-legacy", false);
|
||||
// If neither -legacy nor -descriptors is set, default to true. If -descriptors is set, use its value.
|
||||
bool make_descriptors = (!args.IsArgSet("-descriptors") && !args.IsArgSet("-legacy")) || (args.IsArgSet("-descriptors") && args.GetBoolArg("-descriptors", true));
|
||||
if (make_legacy && make_descriptors) {
|
||||
tfm::format(std::cerr, "Only one of -legacy or -descriptors can be set to true, not both\n");
|
||||
return false;
|
||||
}
|
||||
if (!make_legacy && !make_descriptors) {
|
||||
tfm::format(std::cerr, "One of -legacy or -descriptors must be set to true (or omitted)\n");
|
||||
return false;
|
||||
}
|
||||
if (make_descriptors) {
|
||||
options.create_flags |= WALLET_FLAG_DESCRIPTORS;
|
||||
options.require_format = DatabaseFormat::SQLITE;
|
||||
}
|
||||
|
||||
std::shared_ptr<CWallet> wallet_instance = MakeWallet(name, path, options);
|
||||
if (wallet_instance) {
|
||||
WalletShowInfo(wallet_instance.get());
|
||||
wallet_instance->Close();
|
||||
}
|
||||
} else if (command == "info") {
|
||||
DatabaseOptions options;
|
||||
options.require_existing = true;
|
||||
std::shared_ptr<CWallet> wallet_instance = MakeWallet(name, path, options);
|
||||
if (!wallet_instance) return false;
|
||||
WalletShowInfo(wallet_instance.get());
|
||||
wallet_instance->Close();
|
||||
} else if (command == "salvage") {
|
||||
#ifdef USE_BDB
|
||||
bilingual_str error;
|
||||
std::vector<bilingual_str> warnings;
|
||||
bool ret = RecoverDatabaseFile(path, error, warnings);
|
||||
if (!ret) {
|
||||
for (const auto& warning : warnings) {
|
||||
tfm::format(std::cerr, "%s\n", warning.original);
|
||||
}
|
||||
if (!error.empty()) {
|
||||
tfm::format(std::cerr, "%s\n", error.original);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
#else
|
||||
tfm::format(std::cerr, "Salvage command is not available as BDB support is not compiled");
|
||||
return false;
|
||||
#endif
|
||||
} else if (command == "dump") {
|
||||
DatabaseOptions options;
|
||||
options.require_existing = true;
|
||||
std::shared_ptr<CWallet> wallet_instance = MakeWallet(name, path, options);
|
||||
if (!wallet_instance) return false;
|
||||
bilingual_str error;
|
||||
bool ret = DumpWallet(*wallet_instance, error);
|
||||
if (!ret && !error.empty()) {
|
||||
tfm::format(std::cerr, "%s\n", error.original);
|
||||
return ret;
|
||||
}
|
||||
tfm::format(std::cout, "The dumpfile may contain private keys. To ensure the safety of your Vexta, do not share the dumpfile.\n");
|
||||
return ret;
|
||||
} else if (command == "createfromdump") {
|
||||
bilingual_str error;
|
||||
std::vector<bilingual_str> warnings;
|
||||
bool ret = CreateFromDump(name, path, error, warnings);
|
||||
for (const auto& warning : warnings) {
|
||||
tfm::format(std::cout, "%s\n", warning.original);
|
||||
}
|
||||
if (!ret && !error.empty()) {
|
||||
tfm::format(std::cerr, "%s\n", error.original);
|
||||
}
|
||||
return ret;
|
||||
} else {
|
||||
tfm::format(std::cerr, "Invalid command: %s\n", command);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace WalletTool
|
||||
17
src/wallet/wallettool.h
Normal file
17
src/wallet/wallettool.h
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) 2016-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_WALLETTOOL_H
|
||||
#define DIGIBYTE_WALLET_WALLETTOOL_H
|
||||
|
||||
#include <wallet/wallet.h>
|
||||
|
||||
namespace WalletTool {
|
||||
|
||||
void WalletShowInfo(CWallet* wallet_instance);
|
||||
bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command);
|
||||
|
||||
} // namespace WalletTool
|
||||
|
||||
#endif // DIGIBYTE_WALLET_WALLETTOOL_H
|
||||
45
src/wallet/walletutil.cpp
Normal file
45
src/wallet/walletutil.cpp
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) 2017-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2017-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <wallet/walletutil.h>
|
||||
|
||||
#include <logging.h>
|
||||
#include <util/system.h>
|
||||
|
||||
fs::path GetWalletDir()
|
||||
{
|
||||
fs::path path;
|
||||
|
||||
if (gArgs.IsArgSet("-walletdir")) {
|
||||
path = gArgs.GetArg("-walletdir", "");
|
||||
if (!fs::is_directory(path)) {
|
||||
// If the path specified doesn't exist, we return the deliberately
|
||||
// invalid empty string.
|
||||
path = "";
|
||||
}
|
||||
} else {
|
||||
path = gArgs.GetDataDirNet();
|
||||
// If a wallets directory exists, use that, otherwise default to GetDataDir
|
||||
if (fs::is_directory(path / "wallets")) {
|
||||
path /= "wallets";
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
bool IsFeatureSupported(int wallet_version, int feature_version)
|
||||
{
|
||||
return wallet_version >= feature_version;
|
||||
}
|
||||
|
||||
WalletFeature GetClosestWalletFeature(int version)
|
||||
{
|
||||
static constexpr std::array wallet_features{FEATURE_LATEST, FEATURE_PRE_SPLIT_KEYPOOL, FEATURE_NO_DEFAULT_KEY, FEATURE_HD_SPLIT, FEATURE_HD, FEATURE_COMPRPUBKEY, FEATURE_WALLETCRYPT, FEATURE_BASE};
|
||||
for (const WalletFeature& wf : wallet_features) {
|
||||
if (version >= wf) return wf;
|
||||
}
|
||||
return static_cast<WalletFeature>(0);
|
||||
}
|
||||
108
src/wallet/walletutil.h
Normal file
108
src/wallet/walletutil.h
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// Copyright (c) 2017-2020 The Bitcoin Core developers
|
||||
// Copyright (c) 2017-2020 The DigiByte Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef DIGIBYTE_WALLET_WALLETUTIL_H
|
||||
#define DIGIBYTE_WALLET_WALLETUTIL_H
|
||||
|
||||
#include <fs.h>
|
||||
#include <script/descriptor.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
/** (client) version numbers for particular wallet features */
|
||||
enum WalletFeature
|
||||
{
|
||||
FEATURE_BASE = 10500, // the earliest version new wallets supports (only useful for getwalletinfo's clientversion output)
|
||||
|
||||
FEATURE_WALLETCRYPT = 40000, // wallet encryption
|
||||
FEATURE_COMPRPUBKEY = 60000, // compressed public keys
|
||||
|
||||
FEATURE_HD = 130000, // Hierarchical key derivation after BIP32 (HD Wallet)
|
||||
|
||||
FEATURE_HD_SPLIT = 139900, // Wallet with HD chain split (change outputs will use m/0'/1'/k)
|
||||
|
||||
FEATURE_NO_DEFAULT_KEY = 159900, // Wallet without a default key written
|
||||
|
||||
FEATURE_PRE_SPLIT_KEYPOOL = 169900, // Upgraded to HD SPLIT and can have a pre-split keypool
|
||||
|
||||
FEATURE_LATEST = FEATURE_PRE_SPLIT_KEYPOOL
|
||||
};
|
||||
|
||||
bool IsFeatureSupported(int wallet_version, int feature_version);
|
||||
WalletFeature GetClosestWalletFeature(int version);
|
||||
|
||||
enum WalletFlags : uint64_t {
|
||||
// wallet flags in the upper section (> 1 << 31) will lead to not opening the wallet if flag is unknown
|
||||
// unknown wallet flags in the lower section <= (1 << 31) will be tolerated
|
||||
|
||||
// will categorize coins as clean (not reused) and dirty (reused), and handle
|
||||
// them with privacy considerations in mind
|
||||
WALLET_FLAG_AVOID_REUSE = (1ULL << 0),
|
||||
|
||||
// Indicates that the metadata has already been upgraded to contain key origins
|
||||
WALLET_FLAG_KEY_ORIGIN_METADATA = (1ULL << 1),
|
||||
|
||||
// Indicates that the descriptor cache has been upgraded to cache last hardened xpubs
|
||||
WALLET_FLAG_LAST_HARDENED_XPUB_CACHED = (1ULL << 2),
|
||||
|
||||
// will enforce the rule that the wallet can't contain any private keys (only watch-only/pubkeys)
|
||||
WALLET_FLAG_DISABLE_PRIVATE_KEYS = (1ULL << 32),
|
||||
|
||||
//! Flag set when a wallet contains no HD seed and no private keys, scripts,
|
||||
//! addresses, and other watch only things, and is therefore "blank."
|
||||
//!
|
||||
//! The only function this flag serves is to distinguish a blank wallet from
|
||||
//! a newly created wallet when the wallet database is loaded, to avoid
|
||||
//! initialization that should only happen on first run.
|
||||
//!
|
||||
//! This flag is also a mandatory flag to prevent previous versions of
|
||||
//! Vexta from opening the wallet, thinking it was newly created, and
|
||||
//! then improperly reinitializing it.
|
||||
WALLET_FLAG_BLANK_WALLET = (1ULL << 33),
|
||||
|
||||
//! Indicate that this wallet supports DescriptorScriptPubKeyMan
|
||||
WALLET_FLAG_DESCRIPTORS = (1ULL << 34),
|
||||
|
||||
//! Indicates that the wallet needs an external signer
|
||||
WALLET_FLAG_EXTERNAL_SIGNER = (1ULL << 35),
|
||||
};
|
||||
|
||||
//! Get the path of the wallet directory.
|
||||
fs::path GetWalletDir();
|
||||
|
||||
/** Descriptor with some wallet metadata */
|
||||
class WalletDescriptor
|
||||
{
|
||||
public:
|
||||
std::shared_ptr<Descriptor> descriptor;
|
||||
uint64_t creation_time = 0;
|
||||
int32_t range_start = 0; // First item in range; start of range, inclusive, i.e. [range_start, range_end). This never changes.
|
||||
int32_t range_end = 0; // Item after the last; end of range, exclusive, i.e. [range_start, range_end). This will increment with each TopUp()
|
||||
int32_t next_index = 0; // Position of the next item to generate
|
||||
DescriptorCache cache;
|
||||
|
||||
void DeserializeDescriptor(const std::string& str)
|
||||
{
|
||||
std::string error;
|
||||
FlatSigningProvider keys;
|
||||
descriptor = Parse(str, keys, error, true);
|
||||
if (!descriptor) {
|
||||
throw std::ios_base::failure("Invalid descriptor: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
SERIALIZE_METHODS(WalletDescriptor, obj)
|
||||
{
|
||||
std::string descriptor_str;
|
||||
SER_WRITE(obj, descriptor_str = obj.descriptor->ToString());
|
||||
READWRITE(descriptor_str, obj.creation_time, obj.next_index, obj.range_start, obj.range_end);
|
||||
SER_READ(obj, obj.DeserializeDescriptor(descriptor_str));
|
||||
}
|
||||
|
||||
WalletDescriptor() {}
|
||||
WalletDescriptor(std::shared_ptr<Descriptor> descriptor, uint64_t creation_time, int32_t range_start, int32_t range_end, int32_t next_index) : descriptor(descriptor), creation_time(creation_time), range_start(range_start), range_end(range_end), next_index(next_index) {}
|
||||
};
|
||||
|
||||
#endif // DIGIBYTE_WALLET_WALLETUTIL_H
|
||||
Loading…
Reference in a new issue