diff options
Diffstat (limited to 'src')
80 files changed, 1593 insertions, 1301 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index a8d6591e98..6f8245de8a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -548,6 +548,7 @@ libbitcoin_common_a_SOURCES = \ key.cpp \ key_io.cpp \ merkleblock.cpp \ + net_types.cpp \ netaddress.cpp \ netbase.cpp \ net_permissions.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 40d44aaa2e..a85a359960 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -207,7 +207,6 @@ test_fuzz_fuzz_LDADD = $(FUZZ_SUITE_LD_COMMON) test_fuzz_fuzz_LDFLAGS = $(FUZZ_SUITE_LDFLAGS_COMMON) $(RUNTIME_LDFLAGS) test_fuzz_fuzz_SOURCES = \ test/fuzz/addition_overflow.cpp \ - test/fuzz/addrdb.cpp \ test/fuzz/addrman.cpp \ test/fuzz/asmap.cpp \ test/fuzz/asmap_direct.cpp \ @@ -338,8 +337,8 @@ bitcoin_test_clean : FORCE check-local: $(BITCOIN_TESTS:.cpp=.cpp.test) if BUILD_BITCOIN_TX - @echo "Running test/util/bitcoin-util-test.py..." - $(PYTHON) $(top_builddir)/test/util/bitcoin-util-test.py + @echo "Running test/util/test_runner.py..." + $(PYTHON) $(top_builddir)/test/util/test_runner.py endif @echo "Running test/util/rpcauth-test.py..." $(PYTHON) $(top_builddir)/test/util/rpcauth-test.py diff --git a/src/addrdb.cpp b/src/addrdb.cpp index a5383be7cf..345dbdfb16 100644 --- a/src/addrdb.cpp +++ b/src/addrdb.cpp @@ -19,64 +19,7 @@ #include <util/settings.h> #include <util/system.h> -CBanEntry::CBanEntry(const UniValue& json) - : nVersion(json["version"].get_int()), nCreateTime(json["ban_created"].get_int64()), - nBanUntil(json["banned_until"].get_int64()) -{ -} - -UniValue CBanEntry::ToJson() const -{ - UniValue json(UniValue::VOBJ); - json.pushKV("version", nVersion); - json.pushKV("ban_created", nCreateTime); - json.pushKV("banned_until", nBanUntil); - return json; -} - namespace { - -static const char* BANMAN_JSON_ADDR_KEY = "address"; - -/** - * Convert a `banmap_t` object to a JSON array. - * @param[in] bans Bans list to convert. - * @return a JSON array, similar to the one returned by the `listbanned` RPC. Suitable for - * passing to `BanMapFromJson()`. - */ -UniValue BanMapToJson(const banmap_t& bans) -{ - UniValue bans_json(UniValue::VARR); - for (const auto& it : bans) { - const auto& address = it.first; - const auto& ban_entry = it.second; - UniValue j = ban_entry.ToJson(); - j.pushKV(BANMAN_JSON_ADDR_KEY, address.ToString()); - bans_json.push_back(j); - } - return bans_json; -} - -/** - * Convert a JSON array to a `banmap_t` object. - * @param[in] bans_json JSON to convert, must be as returned by `BanMapToJson()`. - * @param[out] bans Bans list to create from the JSON. - * @throws std::runtime_error if the JSON does not have the expected fields or they contain - * unparsable values. - */ -void BanMapFromJson(const UniValue& bans_json, banmap_t& bans) -{ - for (const auto& ban_entry_json : bans_json.getValues()) { - CSubNet subnet; - const auto& subnet_str = ban_entry_json[BANMAN_JSON_ADDR_KEY].get_str(); - if (!LookupSubNet(subnet_str, subnet)) { - throw std::runtime_error( - strprintf("Cannot parse banned address or subnet: %s", subnet_str)); - } - bans.insert_or_assign(subnet, CBanEntry{ban_entry_json}); - } -} - template <typename Stream, typename Data> bool SerializeDB(Stream& stream, const Data& data) { diff --git a/src/addrdb.h b/src/addrdb.h index 1e0ccb1f60..26b1c5880f 100644 --- a/src/addrdb.h +++ b/src/addrdb.h @@ -8,62 +8,14 @@ #include <fs.h> #include <net_types.h> // For banmap_t -#include <serialize.h> #include <univalue.h> -#include <string> #include <vector> class CAddress; class CAddrMan; class CDataStream; -class CBanEntry -{ -public: - static const int CURRENT_VERSION=1; - int nVersion; - int64_t nCreateTime; - int64_t nBanUntil; - - CBanEntry() - { - SetNull(); - } - - explicit CBanEntry(int64_t nCreateTimeIn) - { - SetNull(); - nCreateTime = nCreateTimeIn; - } - - /** - * Create a ban entry from JSON. - * @param[in] json A JSON representation of a ban entry, as created by `ToJson()`. - * @throw std::runtime_error if the JSON does not have the expected fields. - */ - explicit CBanEntry(const UniValue& json); - - SERIALIZE_METHODS(CBanEntry, obj) - { - uint8_t ban_reason = 2; //! For backward compatibility - READWRITE(obj.nVersion, obj.nCreateTime, obj.nBanUntil, ban_reason); - } - - void SetNull() - { - nVersion = CBanEntry::CURRENT_VERSION; - nCreateTime = 0; - nBanUntil = 0; - } - - /** - * Generate a JSON representation of this ban entry. - * @return JSON suitable for passing to the `CBanEntry(const UniValue&)` constructor. - */ - UniValue ToJson() const; -}; - /** Access to the (IP) address database (peers.dat) */ class CAddrDB { diff --git a/src/addrman.cpp b/src/addrman.cpp index 67473ab2aa..986a1a5d4b 100644 --- a/src/addrman.cpp +++ b/src/addrman.cpp @@ -15,6 +15,27 @@ #include <unordered_map> #include <unordered_set> +/** Over how many buckets entries with tried addresses from a single group (/16 for IPv4) are spread */ +static constexpr uint32_t ADDRMAN_TRIED_BUCKETS_PER_GROUP{8}; +/** Over how many buckets entries with new addresses originating from a single group are spread */ +static constexpr uint32_t ADDRMAN_NEW_BUCKETS_PER_SOURCE_GROUP{64}; +/** Maximum number of times an address can be added to the new table */ +static constexpr int32_t ADDRMAN_NEW_BUCKETS_PER_ADDRESS{8}; +/** How old addresses can maximally be */ +static constexpr int64_t ADDRMAN_HORIZON_DAYS{30}; +/** After how many failed attempts we give up on a new node */ +static constexpr int32_t ADDRMAN_RETRIES{3}; +/** How many successive failures are allowed ... */ +static constexpr int32_t ADDRMAN_MAX_FAILURES{10}; +/** ... in at least this many days */ +static constexpr int64_t ADDRMAN_MIN_FAIL_DAYS{7}; +/** How recent a successful connection should be before we allow an address to be evicted from tried */ +static constexpr int64_t ADDRMAN_REPLACEMENT_HOURS{4}; +/** The maximum number of tried addr collisions to store */ +static constexpr size_t ADDRMAN_SET_TRIED_COLLISION_SIZE{10}; +/** The maximum time we'll spend trying to resolve a tried table collision, in seconds */ +static constexpr int64_t ADDRMAN_TEST_WINDOW{40*60}; // 40 minutes + int CAddrInfo::GetTriedBucket(const uint256& nKey, const std::vector<bool> &asmap) const { uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) << nKey << GetKey()).GetCheapHash(); @@ -95,6 +116,285 @@ CAddrMan::CAddrMan(std::vector<bool> asmap, bool deterministic, int32_t consiste } } +template <typename Stream> +void CAddrMan::Serialize(Stream& s_) const +{ + LOCK(cs); + + /** + * Serialized format. + * * format version byte (@see `Format`) + * * lowest compatible format version byte. This is used to help old software decide + * whether to parse the file. For example: + * * Bitcoin Core version N knows how to parse up to format=3. If a new format=4 is + * introduced in version N+1 that is compatible with format=3 and it is known that + * version N will be able to parse it, then version N+1 will write + * (format=4, lowest_compatible=3) in the first two bytes of the file, and so + * version N will still try to parse it. + * * Bitcoin Core version N+2 introduces a new incompatible format=5. It will write + * (format=5, lowest_compatible=5) and so any versions that do not know how to parse + * format=5 will not try to read the file. + * * nKey + * * nNew + * * nTried + * * number of "new" buckets XOR 2**30 + * * all new addresses (total count: nNew) + * * all tried addresses (total count: nTried) + * * for each new bucket: + * * number of elements + * * for each element: index in the serialized "all new addresses" + * * asmap checksum + * + * 2**30 is xorred with the number of buckets to make addrman deserializer v0 detect it + * as incompatible. This is necessary because it did not check the version number on + * deserialization. + * + * vvNew, vvTried, mapInfo, mapAddr and vRandom are never encoded explicitly; + * they are instead reconstructed from the other information. + * + * This format is more complex, but significantly smaller (at most 1.5 MiB), and supports + * changes to the ADDRMAN_ parameters without breaking the on-disk structure. + * + * We don't use SERIALIZE_METHODS since the serialization and deserialization code has + * very little in common. + */ + + // Always serialize in the latest version (FILE_FORMAT). + + OverrideStream<Stream> s(&s_, s_.GetType(), s_.GetVersion() | ADDRV2_FORMAT); + + s << static_cast<uint8_t>(FILE_FORMAT); + + // Increment `lowest_compatible` iff a newly introduced format is incompatible with + // the previous one. + static constexpr uint8_t lowest_compatible = Format::V3_BIP155; + s << static_cast<uint8_t>(INCOMPATIBILITY_BASE + lowest_compatible); + + s << nKey; + s << nNew; + s << nTried; + + int nUBuckets = ADDRMAN_NEW_BUCKET_COUNT ^ (1 << 30); + s << nUBuckets; + std::unordered_map<int, int> mapUnkIds; + int nIds = 0; + for (const auto& entry : mapInfo) { + mapUnkIds[entry.first] = nIds; + const CAddrInfo &info = entry.second; + if (info.nRefCount) { + assert(nIds != nNew); // this means nNew was wrong, oh ow + s << info; + nIds++; + } + } + nIds = 0; + for (const auto& entry : mapInfo) { + const CAddrInfo &info = entry.second; + if (info.fInTried) { + assert(nIds != nTried); // this means nTried was wrong, oh ow + s << info; + nIds++; + } + } + for (int bucket = 0; bucket < ADDRMAN_NEW_BUCKET_COUNT; bucket++) { + int nSize = 0; + for (int i = 0; i < ADDRMAN_BUCKET_SIZE; i++) { + if (vvNew[bucket][i] != -1) + nSize++; + } + s << nSize; + for (int i = 0; i < ADDRMAN_BUCKET_SIZE; i++) { + if (vvNew[bucket][i] != -1) { + int nIndex = mapUnkIds[vvNew[bucket][i]]; + s << nIndex; + } + } + } + // Store asmap checksum after bucket entries so that it + // can be ignored by older clients for backward compatibility. + uint256 asmap_checksum; + if (m_asmap.size() != 0) { + asmap_checksum = SerializeHash(m_asmap); + } + s << asmap_checksum; +} + +template <typename Stream> +void CAddrMan::Unserialize(Stream& s_) +{ + LOCK(cs); + + assert(vRandom.empty()); + + Format format; + s_ >> Using<CustomUintFormatter<1>>(format); + + int stream_version = s_.GetVersion(); + if (format >= Format::V3_BIP155) { + // Add ADDRV2_FORMAT to the version so that the CNetAddr and CAddress + // unserialize methods know that an address in addrv2 format is coming. + stream_version |= ADDRV2_FORMAT; + } + + OverrideStream<Stream> s(&s_, s_.GetType(), stream_version); + + uint8_t compat; + s >> compat; + const uint8_t lowest_compatible = compat - INCOMPATIBILITY_BASE; + if (lowest_compatible > FILE_FORMAT) { + throw std::ios_base::failure(strprintf( + "Unsupported format of addrman database: %u. It is compatible with formats >=%u, " + "but the maximum supported by this version of %s is %u.", + format, lowest_compatible, PACKAGE_NAME, static_cast<uint8_t>(FILE_FORMAT))); + } + + s >> nKey; + s >> nNew; + s >> nTried; + int nUBuckets = 0; + s >> nUBuckets; + if (format >= Format::V1_DETERMINISTIC) { + nUBuckets ^= (1 << 30); + } + + if (nNew > ADDRMAN_NEW_BUCKET_COUNT * ADDRMAN_BUCKET_SIZE || nNew < 0) { + throw std::ios_base::failure( + strprintf("Corrupt CAddrMan serialization: nNew=%d, should be in [0, %d]", + nNew, + ADDRMAN_NEW_BUCKET_COUNT * ADDRMAN_BUCKET_SIZE)); + } + + if (nTried > ADDRMAN_TRIED_BUCKET_COUNT * ADDRMAN_BUCKET_SIZE || nTried < 0) { + throw std::ios_base::failure( + strprintf("Corrupt CAddrMan serialization: nTried=%d, should be in [0, %d]", + nTried, + ADDRMAN_TRIED_BUCKET_COUNT * ADDRMAN_BUCKET_SIZE)); + } + + // Deserialize entries from the new table. + for (int n = 0; n < nNew; n++) { + CAddrInfo &info = mapInfo[n]; + s >> info; + mapAddr[info] = n; + info.nRandomPos = vRandom.size(); + vRandom.push_back(n); + } + nIdCount = nNew; + + // Deserialize entries from the tried table. + int nLost = 0; + for (int n = 0; n < nTried; n++) { + CAddrInfo info; + s >> info; + int nKBucket = info.GetTriedBucket(nKey, m_asmap); + int nKBucketPos = info.GetBucketPosition(nKey, false, nKBucket); + if (info.IsValid() + && vvTried[nKBucket][nKBucketPos] == -1) { + info.nRandomPos = vRandom.size(); + info.fInTried = true; + vRandom.push_back(nIdCount); + mapInfo[nIdCount] = info; + mapAddr[info] = nIdCount; + vvTried[nKBucket][nKBucketPos] = nIdCount; + nIdCount++; + } else { + nLost++; + } + } + nTried -= nLost; + + // Store positions in the new table buckets to apply later (if possible). + // An entry may appear in up to ADDRMAN_NEW_BUCKETS_PER_ADDRESS buckets, + // so we store all bucket-entry_index pairs to iterate through later. + std::vector<std::pair<int, int>> bucket_entries; + + for (int bucket = 0; bucket < nUBuckets; ++bucket) { + int num_entries{0}; + s >> num_entries; + for (int n = 0; n < num_entries; ++n) { + int entry_index{0}; + s >> entry_index; + if (entry_index >= 0 && entry_index < nNew) { + bucket_entries.emplace_back(bucket, entry_index); + } + } + } + + // If the bucket count and asmap checksum haven't changed, then attempt + // to restore the entries to the buckets/positions they were in before + // serialization. + uint256 supplied_asmap_checksum; + if (m_asmap.size() != 0) { + supplied_asmap_checksum = SerializeHash(m_asmap); + } + uint256 serialized_asmap_checksum; + if (format >= Format::V2_ASMAP) { + s >> serialized_asmap_checksum; + } + const bool restore_bucketing{nUBuckets == ADDRMAN_NEW_BUCKET_COUNT && + serialized_asmap_checksum == supplied_asmap_checksum}; + + if (!restore_bucketing) { + LogPrint(BCLog::ADDRMAN, "Bucketing method was updated, re-bucketing addrman entries from disk\n"); + } + + for (auto bucket_entry : bucket_entries) { + int bucket{bucket_entry.first}; + const int entry_index{bucket_entry.second}; + CAddrInfo& info = mapInfo[entry_index]; + + // Don't store the entry in the new bucket if it's not a valid address for our addrman + if (!info.IsValid()) continue; + + // The entry shouldn't appear in more than + // ADDRMAN_NEW_BUCKETS_PER_ADDRESS. If it has already, just skip + // this bucket_entry. + if (info.nRefCount >= ADDRMAN_NEW_BUCKETS_PER_ADDRESS) continue; + + int bucket_position = info.GetBucketPosition(nKey, true, bucket); + if (restore_bucketing && vvNew[bucket][bucket_position] == -1) { + // Bucketing has not changed, using existing bucket positions for the new table + vvNew[bucket][bucket_position] = entry_index; + ++info.nRefCount; + } else { + // In case the new table data cannot be used (bucket count wrong or new asmap), + // try to give them a reference based on their primary source address. + bucket = info.GetNewBucket(nKey, m_asmap); + bucket_position = info.GetBucketPosition(nKey, true, bucket); + if (vvNew[bucket][bucket_position] == -1) { + vvNew[bucket][bucket_position] = entry_index; + ++info.nRefCount; + } + } + } + + // Prune new entries with refcount 0 (as a result of collisions or invalid address). + int nLostUnk = 0; + for (auto it = mapInfo.cbegin(); it != mapInfo.cend(); ) { + if (it->second.fInTried == false && it->second.nRefCount == 0) { + const auto itCopy = it++; + Delete(itCopy->first); + ++nLostUnk; + } else { + ++it; + } + } + if (nLost + nLostUnk > 0) { + LogPrint(BCLog::ADDRMAN, "addrman lost %i new and %i tried addresses due to collisions or invalid addresses\n", nLostUnk, nLost); + } + + Check(); +} + +// explicit instantiation +template void CAddrMan::Serialize(CHashWriter& s) const; +template void CAddrMan::Serialize(CAutoFile& s) const; +template void CAddrMan::Serialize(CDataStream& s) const; +template void CAddrMan::Unserialize(CAutoFile& s); +template void CAddrMan::Unserialize(CHashVerifier<CAutoFile>& s); +template void CAddrMan::Unserialize(CDataStream& s); +template void CAddrMan::Unserialize(CHashVerifier<CDataStream>& s); + CAddrInfo* CAddrMan::Find(const CNetAddr& addr, int* pnId) { AssertLockHeld(cs); diff --git a/src/addrman.h b/src/addrman.h index 3776e478ce..74bfe9748b 100644 --- a/src/addrman.h +++ b/src/addrman.h @@ -131,49 +131,17 @@ public: * configuration option will introduce (expensive) consistency checks for the entire data structure. */ -//! total number of buckets for tried addresses -#define ADDRMAN_TRIED_BUCKET_COUNT_LOG2 8 +/** Total number of buckets for tried addresses */ +static constexpr int32_t ADDRMAN_TRIED_BUCKET_COUNT_LOG2{8}; +static constexpr int ADDRMAN_TRIED_BUCKET_COUNT{1 << ADDRMAN_TRIED_BUCKET_COUNT_LOG2}; -//! total number of buckets for new addresses -#define ADDRMAN_NEW_BUCKET_COUNT_LOG2 10 +/** Total number of buckets for new addresses */ +static constexpr int32_t ADDRMAN_NEW_BUCKET_COUNT_LOG2{10}; +static constexpr int ADDRMAN_NEW_BUCKET_COUNT{1 << ADDRMAN_NEW_BUCKET_COUNT_LOG2}; -//! maximum allowed number of entries in buckets for new and tried addresses -#define ADDRMAN_BUCKET_SIZE_LOG2 6 - -//! over how many buckets entries with tried addresses from a single group (/16 for IPv4) are spread -#define ADDRMAN_TRIED_BUCKETS_PER_GROUP 8 - -//! over how many buckets entries with new addresses originating from a single group are spread -#define ADDRMAN_NEW_BUCKETS_PER_SOURCE_GROUP 64 - -//! in how many buckets for entries with new addresses a single address may occur -#define ADDRMAN_NEW_BUCKETS_PER_ADDRESS 8 - -//! how old addresses can maximally be -#define ADDRMAN_HORIZON_DAYS 30 - -//! after how many failed attempts we give up on a new node -#define ADDRMAN_RETRIES 3 - -//! how many successive failures are allowed ... -#define ADDRMAN_MAX_FAILURES 10 - -//! ... in at least this many days -#define ADDRMAN_MIN_FAIL_DAYS 7 - -//! how recent a successful connection should be before we allow an address to be evicted from tried -#define ADDRMAN_REPLACEMENT_HOURS 4 - -//! Convenience -#define ADDRMAN_TRIED_BUCKET_COUNT (1 << ADDRMAN_TRIED_BUCKET_COUNT_LOG2) -#define ADDRMAN_NEW_BUCKET_COUNT (1 << ADDRMAN_NEW_BUCKET_COUNT_LOG2) -#define ADDRMAN_BUCKET_SIZE (1 << ADDRMAN_BUCKET_SIZE_LOG2) - -//! the maximum number of tried addr collisions to store -#define ADDRMAN_SET_TRIED_COLLISION_SIZE 10 - -//! the maximum time we'll spend trying to resolve a tried table collision, in seconds -static const int64_t ADDRMAN_TEST_WINDOW = 40*60; // 40 minutes +/** Maximum allowed number of entries in buckets for new and tried addresses */ +static constexpr int32_t ADDRMAN_BUCKET_SIZE_LOG2{6}; +static constexpr int ADDRMAN_BUCKET_SIZE{1 << ADDRMAN_BUCKET_SIZE_LOG2}; /** * Stochastical (IP) address manager @@ -184,276 +152,11 @@ public: // Read asmap from provided binary file static std::vector<bool> DecodeAsmap(fs::path path); - /** - * Serialized format. - * * format version byte (@see `Format`) - * * lowest compatible format version byte. This is used to help old software decide - * whether to parse the file. For example: - * * Bitcoin Core version N knows how to parse up to format=3. If a new format=4 is - * introduced in version N+1 that is compatible with format=3 and it is known that - * version N will be able to parse it, then version N+1 will write - * (format=4, lowest_compatible=3) in the first two bytes of the file, and so - * version N will still try to parse it. - * * Bitcoin Core version N+2 introduces a new incompatible format=5. It will write - * (format=5, lowest_compatible=5) and so any versions that do not know how to parse - * format=5 will not try to read the file. - * * nKey - * * nNew - * * nTried - * * number of "new" buckets XOR 2**30 - * * all new addresses (total count: nNew) - * * all tried addresses (total count: nTried) - * * for each new bucket: - * * number of elements - * * for each element: index in the serialized "all new addresses" - * * asmap checksum - * - * 2**30 is xorred with the number of buckets to make addrman deserializer v0 detect it - * as incompatible. This is necessary because it did not check the version number on - * deserialization. - * - * vvNew, vvTried, mapInfo, mapAddr and vRandom are never encoded explicitly; - * they are instead reconstructed from the other information. - * - * This format is more complex, but significantly smaller (at most 1.5 MiB), and supports - * changes to the ADDRMAN_ parameters without breaking the on-disk structure. - * - * We don't use SERIALIZE_METHODS since the serialization and deserialization code has - * very little in common. - */ template <typename Stream> - void Serialize(Stream& s_) const - EXCLUSIVE_LOCKS_REQUIRED(!cs) - { - LOCK(cs); - - // Always serialize in the latest version (FILE_FORMAT). - - OverrideStream<Stream> s(&s_, s_.GetType(), s_.GetVersion() | ADDRV2_FORMAT); - - s << static_cast<uint8_t>(FILE_FORMAT); - - // Increment `lowest_compatible` iff a newly introduced format is incompatible with - // the previous one. - static constexpr uint8_t lowest_compatible = Format::V3_BIP155; - s << static_cast<uint8_t>(INCOMPATIBILITY_BASE + lowest_compatible); - - s << nKey; - s << nNew; - s << nTried; - - int nUBuckets = ADDRMAN_NEW_BUCKET_COUNT ^ (1 << 30); - s << nUBuckets; - std::unordered_map<int, int> mapUnkIds; - int nIds = 0; - for (const auto& entry : mapInfo) { - mapUnkIds[entry.first] = nIds; - const CAddrInfo &info = entry.second; - if (info.nRefCount) { - assert(nIds != nNew); // this means nNew was wrong, oh ow - s << info; - nIds++; - } - } - nIds = 0; - for (const auto& entry : mapInfo) { - const CAddrInfo &info = entry.second; - if (info.fInTried) { - assert(nIds != nTried); // this means nTried was wrong, oh ow - s << info; - nIds++; - } - } - for (int bucket = 0; bucket < ADDRMAN_NEW_BUCKET_COUNT; bucket++) { - int nSize = 0; - for (int i = 0; i < ADDRMAN_BUCKET_SIZE; i++) { - if (vvNew[bucket][i] != -1) - nSize++; - } - s << nSize; - for (int i = 0; i < ADDRMAN_BUCKET_SIZE; i++) { - if (vvNew[bucket][i] != -1) { - int nIndex = mapUnkIds[vvNew[bucket][i]]; - s << nIndex; - } - } - } - // Store asmap checksum after bucket entries so that it - // can be ignored by older clients for backward compatibility. - uint256 asmap_checksum; - if (m_asmap.size() != 0) { - asmap_checksum = SerializeHash(m_asmap); - } - s << asmap_checksum; - } + void Serialize(Stream& s_) const EXCLUSIVE_LOCKS_REQUIRED(!cs); template <typename Stream> - void Unserialize(Stream& s_) - EXCLUSIVE_LOCKS_REQUIRED(!cs) - { - LOCK(cs); - - assert(vRandom.empty()); - - Format format; - s_ >> Using<CustomUintFormatter<1>>(format); - - int stream_version = s_.GetVersion(); - if (format >= Format::V3_BIP155) { - // Add ADDRV2_FORMAT to the version so that the CNetAddr and CAddress - // unserialize methods know that an address in addrv2 format is coming. - stream_version |= ADDRV2_FORMAT; - } - - OverrideStream<Stream> s(&s_, s_.GetType(), stream_version); - - uint8_t compat; - s >> compat; - const uint8_t lowest_compatible = compat - INCOMPATIBILITY_BASE; - if (lowest_compatible > FILE_FORMAT) { - throw std::ios_base::failure(strprintf( - "Unsupported format of addrman database: %u. It is compatible with formats >=%u, " - "but the maximum supported by this version of %s is %u.", - format, lowest_compatible, PACKAGE_NAME, static_cast<uint8_t>(FILE_FORMAT))); - } - - s >> nKey; - s >> nNew; - s >> nTried; - int nUBuckets = 0; - s >> nUBuckets; - if (format >= Format::V1_DETERMINISTIC) { - nUBuckets ^= (1 << 30); - } - - if (nNew > ADDRMAN_NEW_BUCKET_COUNT * ADDRMAN_BUCKET_SIZE || nNew < 0) { - throw std::ios_base::failure( - strprintf("Corrupt CAddrMan serialization: nNew=%d, should be in [0, %u]", - nNew, - ADDRMAN_NEW_BUCKET_COUNT * ADDRMAN_BUCKET_SIZE)); - } - - if (nTried > ADDRMAN_TRIED_BUCKET_COUNT * ADDRMAN_BUCKET_SIZE || nTried < 0) { - throw std::ios_base::failure( - strprintf("Corrupt CAddrMan serialization: nTried=%d, should be in [0, %u]", - nTried, - ADDRMAN_TRIED_BUCKET_COUNT * ADDRMAN_BUCKET_SIZE)); - } - - // Deserialize entries from the new table. - for (int n = 0; n < nNew; n++) { - CAddrInfo &info = mapInfo[n]; - s >> info; - mapAddr[info] = n; - info.nRandomPos = vRandom.size(); - vRandom.push_back(n); - } - nIdCount = nNew; - - // Deserialize entries from the tried table. - int nLost = 0; - for (int n = 0; n < nTried; n++) { - CAddrInfo info; - s >> info; - int nKBucket = info.GetTriedBucket(nKey, m_asmap); - int nKBucketPos = info.GetBucketPosition(nKey, false, nKBucket); - if (info.IsValid() - && vvTried[nKBucket][nKBucketPos] == -1) { - info.nRandomPos = vRandom.size(); - info.fInTried = true; - vRandom.push_back(nIdCount); - mapInfo[nIdCount] = info; - mapAddr[info] = nIdCount; - vvTried[nKBucket][nKBucketPos] = nIdCount; - nIdCount++; - } else { - nLost++; - } - } - nTried -= nLost; - - // Store positions in the new table buckets to apply later (if possible). - // An entry may appear in up to ADDRMAN_NEW_BUCKETS_PER_ADDRESS buckets, - // so we store all bucket-entry_index pairs to iterate through later. - std::vector<std::pair<int, int>> bucket_entries; - - for (int bucket = 0; bucket < nUBuckets; ++bucket) { - int num_entries{0}; - s >> num_entries; - for (int n = 0; n < num_entries; ++n) { - int entry_index{0}; - s >> entry_index; - if (entry_index >= 0 && entry_index < nNew) { - bucket_entries.emplace_back(bucket, entry_index); - } - } - } - - // If the bucket count and asmap checksum haven't changed, then attempt - // to restore the entries to the buckets/positions they were in before - // serialization. - uint256 supplied_asmap_checksum; - if (m_asmap.size() != 0) { - supplied_asmap_checksum = SerializeHash(m_asmap); - } - uint256 serialized_asmap_checksum; - if (format >= Format::V2_ASMAP) { - s >> serialized_asmap_checksum; - } - const bool restore_bucketing{nUBuckets == ADDRMAN_NEW_BUCKET_COUNT && - serialized_asmap_checksum == supplied_asmap_checksum}; - - if (!restore_bucketing) { - LogPrint(BCLog::ADDRMAN, "Bucketing method was updated, re-bucketing addrman entries from disk\n"); - } - - for (auto bucket_entry : bucket_entries) { - int bucket{bucket_entry.first}; - const int entry_index{bucket_entry.second}; - CAddrInfo& info = mapInfo[entry_index]; - - // Don't store the entry in the new bucket if it's not a valid address for our addrman - if (!info.IsValid()) continue; - - // The entry shouldn't appear in more than - // ADDRMAN_NEW_BUCKETS_PER_ADDRESS. If it has already, just skip - // this bucket_entry. - if (info.nRefCount >= ADDRMAN_NEW_BUCKETS_PER_ADDRESS) continue; - - int bucket_position = info.GetBucketPosition(nKey, true, bucket); - if (restore_bucketing && vvNew[bucket][bucket_position] == -1) { - // Bucketing has not changed, using existing bucket positions for the new table - vvNew[bucket][bucket_position] = entry_index; - ++info.nRefCount; - } else { - // In case the new table data cannot be used (bucket count wrong or new asmap), - // try to give them a reference based on their primary source address. - bucket = info.GetNewBucket(nKey, m_asmap); - bucket_position = info.GetBucketPosition(nKey, true, bucket); - if (vvNew[bucket][bucket_position] == -1) { - vvNew[bucket][bucket_position] = entry_index; - ++info.nRefCount; - } - } - } - - // Prune new entries with refcount 0 (as a result of collisions or invalid address). - int nLostUnk = 0; - for (auto it = mapInfo.cbegin(); it != mapInfo.cend(); ) { - if (it->second.fInTried == false && it->second.nRefCount == 0) { - const auto itCopy = it++; - Delete(itCopy->first); - ++nLostUnk; - } else { - ++it; - } - } - if (nLost + nLostUnk > 0) { - LogPrint(BCLog::ADDRMAN, "addrman lost %i new and %i tried addresses due to collisions or invalid addresses\n", nLostUnk, nLost); - } - - Check(); - } + void Unserialize(Stream& s_) EXCLUSIVE_LOCKS_REQUIRED(!cs); explicit CAddrMan(std::vector<bool> asmap, bool deterministic, int32_t consistency_check_ratio); diff --git a/src/bench/coin_selection.cpp b/src/bench/coin_selection.cpp index 5beb833b48..aa79aab755 100644 --- a/src/bench/coin_selection.cpp +++ b/src/bench/coin_selection.cpp @@ -6,6 +6,7 @@ #include <interfaces/chain.h> #include <node/context.h> #include <wallet/coinselection.h> +#include <wallet/spend.h> #include <wallet/wallet.h> #include <set> @@ -17,7 +18,7 @@ static void addCoin(const CAmount& nValue, const CWallet& wallet, std::vector<st tx.nLockTime = nextLockTime++; // so all transactions get different hashes tx.vout.resize(1); tx.vout[0].nValue = nValue; - wtxs.push_back(std::make_unique<CWalletTx>(&wallet, MakeTransactionRef(std::move(tx)))); + wtxs.push_back(std::make_unique<CWalletTx>(MakeTransactionRef(std::move(tx)))); } // Simple benchmark for wallet coin selection. Note that it maybe be necessary @@ -45,7 +46,7 @@ static void CoinSelection(benchmark::Bench& bench) // Create coins std::vector<COutput> coins; for (const auto& wtx : wtxs) { - coins.emplace_back(wtx.get(), 0 /* iIn */, 6 * 24 /* nDepthIn */, true /* spendable */, true /* solvable */, true /* safe */); + coins.emplace_back(wallet, *wtx, 0 /* iIn */, 6 * 24 /* nDepthIn */, true /* spendable */, true /* solvable */, true /* safe */); } const CoinEligibilityFilter filter_standard(1, 6, 0); @@ -56,7 +57,7 @@ static void CoinSelection(benchmark::Bench& bench) bench.run([&] { std::set<CInputCoin> setCoinsRet; CAmount nValueRet; - bool success = wallet.AttemptSelection(1003 * COIN, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params); + bool success = AttemptSelection(wallet, 1003 * COIN, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params); assert(success); assert(nValueRet == 1003 * COIN); assert(setCoinsRet.size() == 2); @@ -75,9 +76,9 @@ static void add_coin(const CAmount& nValue, int nInput, std::vector<OutputGroup> CMutableTransaction tx; tx.vout.resize(nInput + 1); tx.vout[nInput].nValue = nValue; - std::unique_ptr<CWalletTx> wtx = std::make_unique<CWalletTx>(&testWallet, MakeTransactionRef(std::move(tx))); + std::unique_ptr<CWalletTx> wtx = std::make_unique<CWalletTx>(MakeTransactionRef(std::move(tx))); set.emplace_back(); - set.back().Insert(COutput(wtx.get(), nInput, 0, true, true, true).GetInputCoin(), 0, true, 0, 0, false); + set.back().Insert(COutput(testWallet, *wtx, nInput, 0, true, true, true).GetInputCoin(), 0, true, 0, 0, false); wtxn.emplace_back(std::move(wtx)); } // Copied from src/wallet/test/coinselector_tests.cpp diff --git a/src/bench/wallet_balance.cpp b/src/bench/wallet_balance.cpp index 362b7c1e15..a205d8b6e7 100644 --- a/src/bench/wallet_balance.cpp +++ b/src/bench/wallet_balance.cpp @@ -9,6 +9,7 @@ #include <test/util/setup_common.h> #include <test/util/wallet.h> #include <validationinterface.h> +#include <wallet/receive.h> #include <wallet/wallet.h> #include <optional> @@ -35,11 +36,11 @@ static void WalletBalance(benchmark::Bench& bench, const bool set_dirty, const b } SyncWithValidationInterfaceQueue(); - auto bal = wallet.GetBalance(); // Cache + auto bal = GetBalance(wallet); // Cache bench.run([&] { if (set_dirty) wallet.MarkDirty(); - bal = wallet.GetBalance(); + bal = GetBalance(wallet); if (add_mine) assert(bal.m_mine_trusted > 0); if (add_watchonly) assert(bal.m_watchonly_trusted > 0); }); diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp index bc0af6398c..297f3066ff 100644 --- a/src/bitcoin-cli.cpp +++ b/src/bitcoin-cli.cpp @@ -386,7 +386,9 @@ private: bool IsVersionSelected() const { return m_details_level == 3 || m_details_level == 4; } bool m_is_asmap_on{false}; size_t m_max_addr_length{0}; - size_t m_max_age_length{3}; + size_t m_max_addr_processed_length{5}; + size_t m_max_addr_rate_limited_length{6}; + size_t m_max_age_length{5}; size_t m_max_id_length{2}; struct Peer { std::string addr; @@ -396,6 +398,8 @@ private: std::string age; double min_ping; double ping; + int64_t addr_processed; + int64_t addr_rate_limited; int64_t last_blck; int64_t last_recv; int64_t last_send; @@ -403,6 +407,7 @@ private: int id; int mapped_as; int version; + bool is_addr_relay_enabled; bool is_bip152_hb_from; bool is_bip152_hb_to; bool is_block_relay; @@ -483,6 +488,8 @@ public: const int peer_id{peer["id"].get_int()}; const int mapped_as{peer["mapped_as"].isNull() ? 0 : peer["mapped_as"].get_int()}; const int version{peer["version"].get_int()}; + const int64_t addr_processed{peer["addr_processed"].isNull() ? 0 : peer["addr_processed"].get_int64()}; + const int64_t addr_rate_limited{peer["addr_rate_limited"].isNull() ? 0 : peer["addr_rate_limited"].get_int64()}; const int64_t conn_time{peer["conntime"].get_int64()}; const int64_t last_blck{peer["last_block"].get_int64()}; const int64_t last_recv{peer["lastrecv"].get_int64()}; @@ -493,10 +500,13 @@ public: const std::string addr{peer["addr"].get_str()}; const std::string age{conn_time == 0 ? "" : ToString((m_time_now - conn_time) / 60)}; const std::string sub_version{peer["subver"].get_str()}; + const bool is_addr_relay_enabled{peer["addr_relay_enabled"].isNull() ? false : peer["addr_relay_enabled"].get_bool()}; const bool is_bip152_hb_from{peer["bip152_hb_from"].get_bool()}; const bool is_bip152_hb_to{peer["bip152_hb_to"].get_bool()}; - m_peers.push_back({addr, sub_version, conn_type, network, age, min_ping, ping, last_blck, last_recv, last_send, last_trxn, peer_id, mapped_as, version, is_bip152_hb_from, is_bip152_hb_to, is_block_relay, is_outbound}); + m_peers.push_back({addr, sub_version, conn_type, network, age, min_ping, ping, addr_processed, addr_rate_limited, last_blck, last_recv, last_send, last_trxn, peer_id, mapped_as, version, is_addr_relay_enabled, is_bip152_hb_from, is_bip152_hb_to, is_block_relay, is_outbound}); m_max_addr_length = std::max(addr.length() + 1, m_max_addr_length); + m_max_addr_processed_length = std::max(ToString(addr_processed).length(), m_max_addr_processed_length); + m_max_addr_rate_limited_length = std::max(ToString(addr_rate_limited).length(), m_max_addr_rate_limited_length); m_max_age_length = std::max(age.length(), m_max_age_length); m_max_id_length = std::max(ToString(peer_id).length(), m_max_id_length); m_is_asmap_on |= (mapped_as != 0); @@ -504,39 +514,46 @@ public: } // Generate report header. - std::string result{strprintf("%s %s%s - %i%s\n\n", PACKAGE_NAME, FormatFullVersion(), ChainToString(), networkinfo["protocolversion"].get_int(), networkinfo["subversion"].get_str())}; + std::string result{strprintf("%s client %s%s - server %i%s\n\n", PACKAGE_NAME, FormatFullVersion(), ChainToString(), networkinfo["protocolversion"].get_int(), networkinfo["subversion"].get_str())}; // Report detailed peer connections list sorted by direction and minimum ping time. if (DetailsRequested() && !m_peers.empty()) { std::sort(m_peers.begin(), m_peers.end()); - result += strprintf("<-> type net mping ping send recv txn blk hb %*s ", m_max_age_length, "age"); + result += strprintf("<-> type net mping ping send recv txn blk hb %*s%*s%*s ", + m_max_addr_processed_length, "addrp", + m_max_addr_rate_limited_length, "addrl", + m_max_age_length, "age"); if (m_is_asmap_on) result += " asmap "; result += strprintf("%*s %-*s%s\n", m_max_id_length, "id", IsAddressSelected() ? m_max_addr_length : 0, IsAddressSelected() ? "address" : "", IsVersionSelected() ? "version" : ""); for (const Peer& peer : m_peers) { std::string version{ToString(peer.version) + peer.sub_version}; result += strprintf( - "%3s %6s %5s%7s%7s%5s%5s%5s%5s %2s %*s%*i %*s %-*s%s\n", + "%3s %6s %5s%7s%7s%5s%5s%5s%5s %2s %*s%*s%*s%*i %*s %-*s%s\n", peer.is_outbound ? "out" : "in", ConnectionTypeForNetinfo(peer.conn_type), peer.network, PingTimeToString(peer.min_ping), PingTimeToString(peer.ping), - peer.last_send == 0 ? "" : ToString(m_time_now - peer.last_send), - peer.last_recv == 0 ? "" : ToString(m_time_now - peer.last_recv), - peer.last_trxn == 0 ? "" : ToString((m_time_now - peer.last_trxn) / 60), - peer.last_blck == 0 ? "" : ToString((m_time_now - peer.last_blck) / 60), + peer.last_send ? ToString(m_time_now - peer.last_send) : "", + peer.last_recv ? ToString(m_time_now - peer.last_recv) : "", + peer.last_trxn ? ToString((m_time_now - peer.last_trxn) / 60) : peer.is_block_relay ? "*" : "", + peer.last_blck ? ToString((m_time_now - peer.last_blck) / 60) : "", strprintf("%s%s", peer.is_bip152_hb_to ? "." : " ", peer.is_bip152_hb_from ? "*" : " "), + m_max_addr_processed_length, // variable spacing + peer.addr_processed ? ToString(peer.addr_processed) : peer.is_addr_relay_enabled ? "" : ".", + m_max_addr_rate_limited_length, // variable spacing + peer.addr_rate_limited ? ToString(peer.addr_rate_limited) : "", m_max_age_length, // variable spacing peer.age, m_is_asmap_on ? 7 : 0, // variable spacing - m_is_asmap_on && peer.mapped_as != 0 ? ToString(peer.mapped_as) : "", + m_is_asmap_on && peer.mapped_as ? ToString(peer.mapped_as) : "", m_max_id_length, // variable spacing peer.id, IsAddressSelected() ? m_max_addr_length : 0, // variable spacing IsAddressSelected() ? peer.addr : "", IsVersionSelected() && version != "0" ? version : ""); } - result += strprintf(" ms ms sec sec min min %*s\n\n", m_max_age_length, "min"); + result += strprintf(" ms ms sec sec min min %*s\n\n", m_max_age_length, "min"); } // Report peer connection totals by type. @@ -610,10 +627,14 @@ public: " send Time since last message sent to the peer, in seconds\n" " recv Time since last message received from the peer, in seconds\n" " txn Time since last novel transaction received from the peer and accepted into our mempool, in minutes\n" + " \"*\" - the peer requested we not relay transactions to it (relaytxes is false)\n" " blk Time since last novel block passing initial validity checks received from the peer, in minutes\n" " hb High-bandwidth BIP152 compact block relay\n" " \".\" (to) - we selected the peer as a high-bandwidth peer\n" " \"*\" (from) - the peer selected us as a high-bandwidth peer\n" + " addrp Total number of addresses processed, excluding those dropped due to rate limiting\n" + " \".\" - we do not relay addresses to this peer (addr_relay_enabled is false)\n" + " addrl Total number of addresses dropped due to rate limiting\n" " age Duration of connection to the peer, in minutes\n" " asmap Mapped AS (Autonomous System) number in the BGP route to the peer, used for diversifying\n" " peer selection (only displayed if the -asmap config option is set)\n" @@ -885,7 +906,7 @@ static void GetWalletBalances(UniValue& result) } /** - * GetProgressBar contructs a progress bar with 5% intervals. + * GetProgressBar constructs a progress bar with 5% intervals. * * @param[in] progress The proportion of the progress bar to be filled between 0 and 1. * @param[out] progress_bar String representation of the progress bar. diff --git a/src/bitcoin-tx.cpp b/src/bitcoin-tx.cpp index cd2cdf01f2..58c51bd8e0 100644 --- a/src/bitcoin-tx.cpp +++ b/src/bitcoin-tx.cpp @@ -772,9 +772,7 @@ static std::string readStdin() if (ferror(stdin)) throw std::runtime_error("error reading stdin"); - boost::algorithm::trim_right(ret); - - return ret; + return TrimString(ret); } static int CommandLineRawTx(int argc, char* argv[]) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index c3bbb147be..4cc37560a3 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -392,7 +392,7 @@ public: consensus.BIP16Exception = uint256(); consensus.BIP34Height = 2; // BIP34 activated on regtest (Block at height 1 not enforced for testing purposes) consensus.BIP34Hash = uint256(); - consensus.BIP65Height = 1351; // BIP65 activated on regtest (Used in functional tests) + consensus.BIP65Height = 111; // BIP65 activated on regtest (Block at height 110 and earlier not enforced for testing purposes) consensus.BIP66Height = 102; // BIP66 activated on regtest (Block at height 101 and earlier not enforced for testing purposes) consensus.CSVHeight = 432; // CSV activated on regtest (Used in rpc activation tests) consensus.SegwitHeight = 0; // SEGWIT is always activated on regtest unless overridden diff --git a/src/dummywallet.cpp b/src/dummywallet.cpp index 95886d3138..2d897f4c40 100644 --- a/src/dummywallet.cpp +++ b/src/dummywallet.cpp @@ -28,6 +28,7 @@ void DummyWalletInit::AddWalletOptions(ArgsManager& argsman) const "-addresstype", "-avoidpartialspends", "-changetype", + "-consolidatefeerate=<amt>", "-disablewallet", "-discardfee=<amt>", "-fallbackfee=<amt>", diff --git a/src/fs.cpp b/src/fs.cpp index 4f20ca4d28..b9b3c46d8d 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -154,7 +154,10 @@ std::string get_filesystem_error_message(const fs::filesystem_error& e) #ifdef __GLIBCXX__ // reference: https://github.com/gcc-mirror/gcc/blob/gcc-7_3_0-release/libstdc%2B%2B-v3/include/std/fstream#L270 - +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch" +#endif static std::string openmodeToStr(std::ios_base::openmode mode) { switch (mode & ~std::ios_base::ate) { @@ -192,6 +195,9 @@ static std::string openmodeToStr(std::ios_base::openmode mode) return std::string(); } } +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic pop +#endif void ifstream::open(const fs::path& p, std::ios_base::openmode mode) { @@ -242,7 +248,11 @@ void ofstream::close() } #else // __GLIBCXX__ +#if BOOST_VERSION >= 107700 +static_assert(sizeof(*BOOST_FILESYSTEM_C_STR(fs::path())) == sizeof(wchar_t), +#else static_assert(sizeof(*fs::path().BOOST_FILESYSTEM_C_STR) == sizeof(wchar_t), +#endif // BOOST_VERSION >= 107700 "Warning: This build is using boost::filesystem ofstream and ifstream " "implementations which will fail to open paths containing multibyte " "characters. You should delete this static_assert to ignore this warning, " diff --git a/src/httprpc.cpp b/src/httprpc.cpp index e11e4acb5c..9ae592be79 100644 --- a/src/httprpc.cpp +++ b/src/httprpc.cpp @@ -10,6 +10,7 @@ #include <rpc/protocol.h> #include <rpc/server.h> #include <util/strencodings.h> +#include <util/string.h> #include <util/system.h> #include <util/translation.h> #include <walletinitinterface.h> @@ -22,7 +23,7 @@ #include <set> #include <string> -#include <boost/algorithm/string.hpp> // boost::trim +#include <boost/algorithm/string.hpp> /** WWW-Authenticate to present with 401 Unauthorized response */ static const char* WWW_AUTH_HEADER_DATA = "Basic realm=\"jsonrpc\""; @@ -130,8 +131,7 @@ static bool RPCAuthorized(const std::string& strAuth, std::string& strAuthUserna return false; if (strAuth.substr(0, 6) != "Basic ") return false; - std::string strUserPass64 = strAuth.substr(6); - boost::trim(strUserPass64); + std::string strUserPass64 = TrimString(strAuth.substr(6)); std::string strUserPass = DecodeBase64(strUserPass64); if (strUserPass.find(':') != std::string::npos) diff --git a/src/key.cpp b/src/key.cpp index 7bef3d529b..40df248e02 100644 --- a/src/key.cpp +++ b/src/key.cpp @@ -357,6 +357,7 @@ void CExtKey::Decode(const unsigned char code[BIP32_EXTKEY_SIZE]) { nChild = (code[5] << 24) | (code[6] << 16) | (code[7] << 8) | code[8]; memcpy(chaincode.begin(), code+9, 32); key.Set(code+42, code+BIP32_EXTKEY_SIZE, true); + if ((nDepth == 0 && (nChild != 0 || ReadLE32(vchFingerprint) != 0)) || code[41] != 0) key = CKey(); } bool ECC_InitSanityCheck() { diff --git a/src/logging.cpp b/src/logging.cpp index b456108b61..eb2c750296 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -159,6 +159,7 @@ const CLogCategoryDesc LogCategories[] = {BCLog::VALIDATION, "validation"}, {BCLog::I2P, "i2p"}, {BCLog::IPC, "ipc"}, + {BCLog::LOCK, "lock"}, {BCLog::ALL, "1"}, {BCLog::ALL, "all"}, }; diff --git a/src/logging.h b/src/logging.h index 38d73863e7..53a89d28bd 100644 --- a/src/logging.h +++ b/src/logging.h @@ -59,6 +59,7 @@ namespace BCLog { VALIDATION = (1 << 21), I2P = (1 << 22), IPC = (1 << 23), + LOCK = (1 << 24), ALL = ~(uint32_t)0, }; diff --git a/src/logging/timer.h b/src/logging/timer.h index 159920e397..647e3fa30e 100644 --- a/src/logging/timer.h +++ b/src/logging/timer.h @@ -58,12 +58,14 @@ public: return strprintf("%s: %s", m_prefix, msg); } - std::string units = ""; + if (std::is_same<TimeType, std::chrono::microseconds>::value) { + return strprintf("%s: %s (%iμs)", m_prefix, msg, end_time.count()); + } + + std::string units; float divisor = 1; - if (std::is_same<TimeType, std::chrono::microseconds>::value) { - units = "μs"; - } else if (std::is_same<TimeType, std::chrono::milliseconds>::value) { + if (std::is_same<TimeType, std::chrono::milliseconds>::value) { units = "ms"; divisor = 1000.; } else if (std::is_same<TimeType, std::chrono::seconds>::value) { @@ -93,6 +95,8 @@ private: } // namespace BCLog +#define LOG_TIME_MICROS_WITH_CATEGORY(end_msg, log_category) \ + BCLog::Timer<std::chrono::microseconds> PASTE2(logging_timer, __COUNTER__)(__func__, end_msg, log_category) #define LOG_TIME_MILLIS_WITH_CATEGORY(end_msg, log_category) \ BCLog::Timer<std::chrono::milliseconds> PASTE2(logging_timer, __COUNTER__)(__func__, end_msg, log_category) #define LOG_TIME_SECONDS(end_msg) \ diff --git a/src/net.cpp b/src/net.cpp index f4745f1f5d..35376b89ac 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -331,7 +331,7 @@ CNode* CConnman::FindNode(const std::string& addrName) { LOCK(cs_vNodes); for (CNode* pnode : vNodes) { - if (pnode->GetAddrName() == addrName) { + if (pnode->m_addr_name == addrName) { return pnode; } } @@ -414,14 +414,10 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo return nullptr; } // It is possible that we already have a connection to the IP/port pszDest resolved to. - // In that case, drop the connection that was just created, and return the existing CNode instead. - // Also store the name we used to connect in that CNode, so that future FindNode() calls to that - // name catch this early. + // In that case, drop the connection that was just created. LOCK(cs_vNodes); CNode* pnode = FindNode(static_cast<CService>(addrConnect)); - if (pnode) - { - pnode->MaybeSetAddrName(std::string(pszDest)); + if (pnode) { LogPrintf("Failed to open new connection, already connected\n"); return nullptr; } @@ -534,19 +530,8 @@ std::string ConnectionTypeAsString(ConnectionType conn_type) assert(false); } -std::string CNode::GetAddrName() const { - LOCK(cs_addrName); - return addrName; -} - -void CNode::MaybeSetAddrName(const std::string& addrNameIn) { - LOCK(cs_addrName); - if (addrName.empty()) { - addrName = addrNameIn; - } -} - -CService CNode::GetAddrLocal() const { +CService CNode::GetAddrLocal() const +{ LOCK(cs_addrLocal); return addrLocal; } @@ -587,7 +572,7 @@ void CNode::CopyStats(CNodeStats& stats, const std::vector<bool>& asmap) X(nLastBlockTime); X(nTimeConnected); X(nTimeOffset); - stats.addrName = GetAddrName(); + X(m_addr_name); X(nVersion); { LOCK(cs_SubVer); @@ -2137,7 +2122,7 @@ std::vector<AddedNodeInfo> CConnman::GetAddedNodeInfo() const if (pnode->addr.IsValid()) { mapConnected[pnode->addr] = pnode->IsInboundConn(); } - std::string addrName = pnode->GetAddrName(); + std::string addrName{pnode->m_addr_name}; if (!addrName.empty()) { mapConnectedByName[std::move(addrName)] = std::make_pair(pnode->IsInboundConn(), static_cast<const CService&>(pnode->addr)); } @@ -2966,6 +2951,7 @@ CNode::CNode(NodeId idIn, ServiceFlags nLocalServicesIn, SOCKET hSocketIn, const : nTimeConnected(GetTimeSeconds()), addr(addrIn), addrBind(addrBindIn), + m_addr_name{addrNameIn.empty() ? addr.ToStringIPPort() : addrNameIn}, m_inbound_onion(inbound_onion), nKeyedNetGroup(nKeyedNetGroupIn), id(idIn), @@ -2975,7 +2961,6 @@ CNode::CNode(NodeId idIn, ServiceFlags nLocalServicesIn, SOCKET hSocketIn, const { if (inbound_onion) assert(conn_type_in == ConnectionType::INBOUND); hSocket = hSocketIn; - addrName = addrNameIn == "" ? addr.ToStringIPPort() : addrNameIn; if (conn_type_in != ConnectionType::BLOCK_RELAY) { m_tx_relay = std::make_unique<TxRelay>(); } @@ -2985,7 +2970,7 @@ CNode::CNode(NodeId idIn, ServiceFlags nLocalServicesIn, SOCKET hSocketIn, const mapRecvBytesPerMsgCmd[NET_MESSAGE_COMMAND_OTHER] = 0; if (fLogIPs) { - LogPrint(BCLog::NET, "Added connection to %s peer=%d\n", addrName, id); + LogPrint(BCLog::NET, "Added connection to %s peer=%d\n", m_addr_name, id); } else { LogPrint(BCLog::NET, "Added connection peer=%d\n", id); } @@ -3014,7 +2999,7 @@ void CConnman::PushMessage(CNode* pnode, CSerializedNetMsg&& msg) TRACE6(net, outbound_message, pnode->GetId(), - pnode->GetAddrName().c_str(), + pnode->m_addr_name.c_str(), pnode->ConnectionTypeAsString().c_str(), msg.m_type.c_str(), msg.data.size(), @@ -248,7 +248,7 @@ public: int64_t nLastBlockTime; int64_t nTimeConnected; int64_t nTimeOffset; - std::string addrName; + std::string m_addr_name; int nVersion; std::string cleanSubVer; bool fInbound; @@ -430,6 +430,7 @@ public: const CAddress addr; // Bind address of our side of the connection const CAddress addrBind; + const std::string m_addr_name; //! Whether this peer is an inbound onion, i.e. connected via our Tor onion service. const bool m_inbound_onion; std::atomic<int> nVersion{0}; @@ -658,10 +659,6 @@ public: return nLocalServices; } - std::string GetAddrName() const; - //! Sets the addrName only if it was not previously set - void MaybeSetAddrName(const std::string& addrNameIn); - std::string ConnectionTypeAsString() const { return ::ConnectionTypeAsString(m_conn_type); } /** A ping-pong round trip has completed successfully. Update latest and minimum ping times. */ @@ -693,10 +690,7 @@ private: //! service advertisements. const ServiceFlags nLocalServices; - std::list<CNetMessage> vRecvMsg; // Used only by SocketHandler thread - - mutable RecursiveMutex cs_addrName; - std::string addrName GUARDED_BY(cs_addrName); + std::list<CNetMessage> vRecvMsg; // Used only by SocketHandler thread // Our address, as reported by the peer CService addrLocal GUARDED_BY(cs_addrLocal); diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 8be82f7ebc..3ad34e83ba 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -4086,7 +4086,7 @@ bool PeerManagerImpl::ProcessMessages(CNode* pfrom, std::atomic<bool>& interrupt TRACE6(net, inbound_message, pfrom->GetId(), - pfrom->GetAddrName().c_str(), + pfrom->m_addr_name.c_str(), pfrom->ConnectionTypeAsString().c_str(), msg.m_command.c_str(), msg.m_recv.size(), diff --git a/src/net_types.cpp b/src/net_types.cpp new file mode 100644 index 0000000000..c8f57fe6c6 --- /dev/null +++ b/src/net_types.cpp @@ -0,0 +1,65 @@ +// Copyright (c) 2021 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include <net_types.h> + +#include <netaddress.h> +#include <netbase.h> +#include <univalue.h> + +CBanEntry::CBanEntry(const UniValue& json) + : nVersion(json["version"].get_int()), nCreateTime(json["ban_created"].get_int64()), + nBanUntil(json["banned_until"].get_int64()) +{ +} + +UniValue CBanEntry::ToJson() const +{ + UniValue json(UniValue::VOBJ); + json.pushKV("version", nVersion); + json.pushKV("ban_created", nCreateTime); + json.pushKV("banned_until", nBanUntil); + return json; +} + +static const char* BANMAN_JSON_ADDR_KEY = "address"; + +/** + * Convert a `banmap_t` object to a JSON array. + * @param[in] bans Bans list to convert. + * @return a JSON array, similar to the one returned by the `listbanned` RPC. Suitable for + * passing to `BanMapFromJson()`. + */ +UniValue BanMapToJson(const banmap_t& bans) +{ + UniValue bans_json(UniValue::VARR); + for (const auto& it : bans) { + const auto& address = it.first; + const auto& ban_entry = it.second; + UniValue j = ban_entry.ToJson(); + j.pushKV(BANMAN_JSON_ADDR_KEY, address.ToString()); + bans_json.push_back(j); + } + return bans_json; +} + +/** + * Convert a JSON array to a `banmap_t` object. + * @param[in] bans_json JSON to convert, must be as returned by `BanMapToJson()`. + * @param[out] bans Bans list to create from the JSON. + * @throws std::runtime_error if the JSON does not have the expected fields or they contain + * unparsable values. + */ +void BanMapFromJson(const UniValue& bans_json, banmap_t& bans) +{ + for (const auto& ban_entry_json : bans_json.getValues()) { + CSubNet subnet; + const auto& subnet_str = ban_entry_json[BANMAN_JSON_ADDR_KEY].get_str(); + if (!LookupSubNet(subnet_str, subnet)) { + throw std::runtime_error( + strprintf("Cannot parse banned address or subnet: %s", subnet_str)); + } + bans.insert_or_assign(subnet, CBanEntry{ban_entry_json}); + } +} diff --git a/src/net_types.h b/src/net_types.h index d55a8cde6c..ffdc24c772 100644 --- a/src/net_types.h +++ b/src/net_types.h @@ -5,11 +5,56 @@ #ifndef BITCOIN_NET_TYPES_H #define BITCOIN_NET_TYPES_H +#include <cstdint> #include <map> -class CBanEntry; class CSubNet; +class UniValue; + +class CBanEntry +{ +public: + static constexpr int CURRENT_VERSION{1}; + int nVersion{CBanEntry::CURRENT_VERSION}; + int64_t nCreateTime{0}; + int64_t nBanUntil{0}; + + CBanEntry() {} + + explicit CBanEntry(int64_t nCreateTimeIn) + : nCreateTime{nCreateTimeIn} {} + + /** + * Create a ban entry from JSON. + * @param[in] json A JSON representation of a ban entry, as created by `ToJson()`. + * @throw std::runtime_error if the JSON does not have the expected fields. + */ + explicit CBanEntry(const UniValue& json); + + /** + * Generate a JSON representation of this ban entry. + * @return JSON suitable for passing to the `CBanEntry(const UniValue&)` constructor. + */ + UniValue ToJson() const; +}; using banmap_t = std::map<CSubNet, CBanEntry>; +/** + * Convert a `banmap_t` object to a JSON array. + * @param[in] bans Bans list to convert. + * @return a JSON array, similar to the one returned by the `listbanned` RPC. Suitable for + * passing to `BanMapFromJson()`. + */ +UniValue BanMapToJson(const banmap_t& bans); + +/** + * Convert a JSON array to a `banmap_t` object. + * @param[in] bans_json JSON to convert, must be as returned by `BanMapToJson()`. + * @param[out] bans Bans list to create from the JSON. + * @throws std::runtime_error if the JSON does not have the expected fields or they contain + * unparsable values. + */ +void BanMapFromJson(const UniValue& bans_json, banmap_t& bans); + #endif // BITCOIN_NET_TYPES_H diff --git a/src/policy/rbf.cpp b/src/policy/rbf.cpp index 8125b41c41..43624c7993 100644 --- a/src/policy/rbf.cpp +++ b/src/policy/rbf.cpp @@ -3,6 +3,10 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include <policy/rbf.h> + +#include <policy/settings.h> +#include <tinyformat.h> +#include <util/moneystr.h> #include <util/rbf.h> RBFTransactionState IsRBFOptIn(const CTransaction& tx, const CTxMemPool& pool) @@ -42,3 +46,34 @@ RBFTransactionState IsRBFOptInEmptyMempool(const CTransaction& tx) // If we don't have a local mempool we can only check the transaction itself. return SignalsOptInRBF(tx) ? RBFTransactionState::REPLACEABLE_BIP125 : RBFTransactionState::UNKNOWN; } + +bool GetEntriesForConflicts(const CTransaction& tx, + CTxMemPool& m_pool, + const CTxMemPool::setEntries& setIterConflicting, + CTxMemPool::setEntries& allConflicting, + std::string& err_string) +{ + AssertLockHeld(m_pool.cs); + const uint256 hash = tx.GetHash(); + uint64_t nConflictingCount = 0; + for (const auto& mi : setIterConflicting) { + nConflictingCount += mi->GetCountWithDescendants(); + // This potentially overestimates the number of actual descendants + // but we just want to be conservative to avoid doing too much + // work. + if (nConflictingCount > MAX_BIP125_REPLACEMENT_CANDIDATES) { + err_string = strprintf("rejecting replacement %s; too many potential replacements (%d > %d)\n", + hash.ToString(), + nConflictingCount, + MAX_BIP125_REPLACEMENT_CANDIDATES); + return false; + } + } + // If not too many to replace, then calculate the set of + // transactions that would have to be evicted + for (CTxMemPool::txiter it : setIterConflicting) { + m_pool.CalculateDescendants(it, allConflicting); + } + return true; +} + diff --git a/src/policy/rbf.h b/src/policy/rbf.h index e078070c1c..a67e9058df 100644 --- a/src/policy/rbf.h +++ b/src/policy/rbf.h @@ -7,6 +7,10 @@ #include <txmempool.h> +/** Maximum number of transactions that can be replaced by BIP125 RBF (Rule #5). This includes all + * mempool conflicts and their descendants. */ +static constexpr uint32_t MAX_BIP125_REPLACEMENT_CANDIDATES{100}; + /** The rbf state of unconfirmed transactions */ enum class RBFTransactionState { /** Unconfirmed tx that does not signal rbf and is not in the mempool */ @@ -31,4 +35,19 @@ enum class RBFTransactionState { RBFTransactionState IsRBFOptIn(const CTransaction& tx, const CTxMemPool& pool) EXCLUSIVE_LOCKS_REQUIRED(pool.cs); RBFTransactionState IsRBFOptInEmptyMempool(const CTransaction& tx); +/** Get all descendants of setIterConflicting. Also enforce BIP125 Rule #5, "The number of original + * transactions to be replaced and their descendant transactions which will be evicted from the + * mempool must not exceed a total of 100 transactions." Quit as early as possible. There cannot be + * more than MAX_BIP125_REPLACEMENT_CANDIDATES potential entries. + * @param[in] setIterConflicting The set of iterators to mempool entries. + * @param[out] err_string Used to return errors, if any. + * @param[out] allConflicting Populated with all the mempool entries that would be replaced, + * which includes descendants of setIterConflicting. Not cleared at + * the start; any existing mempool entries will remain in the set. + * @returns false if Rule 5 is broken. + */ +bool GetEntriesForConflicts(const CTransaction& tx, CTxMemPool& m_pool, + const CTxMemPool::setEntries& setIterConflicting, + CTxMemPool::setEntries& allConflicting, + std::string& err_string) EXCLUSIVE_LOCKS_REQUIRED(m_pool.cs); #endif // BITCOIN_POLICY_RBF_H diff --git a/src/protocol.h b/src/protocol.h index 126c3646a4..2149e45993 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -436,7 +436,7 @@ public: SerReadWriteMany(os, ser_action, ReadWriteAsHelper<CService>(obj)); } - //! Always included in serialization, except in the network format on INIT_PROTO_VERSION. + //! Always included in serialization. uint32_t nTime{TIME_INIT}; //! Serialized as uint64_t in V1, and as CompactSize in V2. ServiceFlags nServices{NODE_NONE}; diff --git a/src/pubkey.cpp b/src/pubkey.cpp index 75202e7cf4..d14a20b870 100644 --- a/src/pubkey.cpp +++ b/src/pubkey.cpp @@ -180,6 +180,23 @@ XOnlyPubKey::XOnlyPubKey(Span<const unsigned char> bytes) std::copy(bytes.begin(), bytes.end(), m_keydata.begin()); } +std::vector<CKeyID> XOnlyPubKey::GetKeyIDs() const +{ + std::vector<CKeyID> out; + // For now, use the old full pubkey-based key derivation logic. As it is indexed by + // Hash160(full pubkey), we need to return both a version prefixed with 0x02, and one + // with 0x03. + unsigned char b[33] = {0x02}; + std::copy(m_keydata.begin(), m_keydata.end(), b + 1); + CPubKey fullpubkey; + fullpubkey.Set(b, b + 33); + out.push_back(fullpubkey.GetID()); + b[0] = 0x03; + fullpubkey.Set(b, b + 33); + out.push_back(fullpubkey.GetID()); + return out; +} + bool XOnlyPubKey::IsFullyValid() const { secp256k1_xonly_pubkey pubkey; @@ -333,6 +350,7 @@ void CExtPubKey::Decode(const unsigned char code[BIP32_EXTKEY_SIZE]) { nChild = (code[5] << 24) | (code[6] << 16) | (code[7] << 8) | code[8]; memcpy(chaincode.begin(), code+9, 32); pubkey.Set(code+41, code+BIP32_EXTKEY_SIZE); + if ((nDepth == 0 && (nChild != 0 || ReadLE32(vchFingerprint) != 0)) || !pubkey.IsFullyValid()) pubkey = CPubKey(); } bool CExtPubKey::Derive(CExtPubKey &out, unsigned int _nChild) const { diff --git a/src/pubkey.h b/src/pubkey.h index eec34a89c2..861a2cf500 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -267,6 +267,11 @@ public: /** Construct a Taproot tweaked output point with this point as internal key. */ std::optional<std::pair<XOnlyPubKey, bool>> CreateTapTweak(const uint256* merkle_root) const; + /** Returns a list of CKeyIDs for the CPubKeys that could have been used to create this XOnlyPubKey. + * This is needed for key lookups since keys are indexed by CKeyID. + */ + std::vector<CKeyID> GetKeyIDs() const; + const unsigned char& operator[](int pos) const { return *(m_keydata.begin() + pos); } const unsigned char* data() const { return m_keydata.begin(); } static constexpr size_t size() { return decltype(m_keydata)::size(); } diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index a92675fb14..55eba60bcd 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -113,6 +113,7 @@ BitcoinGUI::BitcoinGUI(interfaces::Node& node, const PlatformStyle *_platformSty connect(walletFrame, &WalletFrame::message, [this](const QString& title, const QString& message, unsigned int style) { this->message(title, message, style); }); + connect(walletFrame, &WalletFrame::currentWalletSet, [this] { updateWalletStatus(); }); setCentralWidget(walletFrame); } else #endif // ENABLE_WALLET @@ -694,7 +695,6 @@ void BitcoinGUI::addWallet(WalletModel* walletModel) }); connect(wallet_view, &WalletView::encryptionStatusChanged, this, &BitcoinGUI::updateWalletStatus); connect(wallet_view, &WalletView::incomingTransaction, this, &BitcoinGUI::incomingTransaction); - connect(wallet_view, &WalletView::hdEnabledStatusChanged, this, &BitcoinGUI::updateWalletStatus); connect(this, &BitcoinGUI::setPrivacy, wallet_view, &WalletView::setPrivacy); wallet_view->setPrivacy(isPrivacyModeActivated()); const QString display_name = walletModel->getDisplayName(); @@ -1340,9 +1340,8 @@ void BitcoinGUI::setEncryptionStatus(int status) void BitcoinGUI::updateWalletStatus() { - if (!walletFrame) { - return; - } + assert(walletFrame); + WalletView * const walletView = walletFrame->currentWalletView(); if (!walletView) { return; diff --git a/src/qt/peertablemodel.cpp b/src/qt/peertablemodel.cpp index 98efaf29d7..433a1ea934 100644 --- a/src/qt/peertablemodel.cpp +++ b/src/qt/peertablemodel.cpp @@ -72,7 +72,7 @@ QVariant PeerTableModel::data(const QModelIndex& index, int role) const case NetNodeId: return (qint64)rec->nodeStats.nodeid; case Address: - return QString::fromStdString(rec->nodeStats.addrName); + return QString::fromStdString(rec->nodeStats.m_addr_name); case Direction: return QString(rec->nodeStats.fInbound ? //: An Inbound Connection from a Peer. diff --git a/src/qt/peertablesortproxy.cpp b/src/qt/peertablesortproxy.cpp index f92eef48f1..419133bc32 100644 --- a/src/qt/peertablesortproxy.cpp +++ b/src/qt/peertablesortproxy.cpp @@ -25,7 +25,7 @@ bool PeerTableSortProxy::lessThan(const QModelIndex& left_index, const QModelInd case PeerTableModel::NetNodeId: return left_stats.nodeid < right_stats.nodeid; case PeerTableModel::Address: - return left_stats.addrName.compare(right_stats.addrName) < 0; + return left_stats.m_addr_name.compare(right_stats.m_addr_name) < 0; case PeerTableModel::Direction: return left_stats.fInbound > right_stats.fInbound; // default sort Inbound, then Outbound case PeerTableModel::ConnectionType: diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index ba3eb80efa..829f7add80 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -706,6 +706,13 @@ void RPCConsole::setClientModel(ClientModel *model, int bestblock_height, int64_ // create ban table context menu banTableContextMenu = new QMenu(this); + /*: Context menu action to copy the IP/Netmask of a banned peer. + IP/Netmask is the combination of a peer's IP address and its Netmask. + For IP address see: https://en.wikipedia.org/wiki/IP_address */ + banTableContextMenu->addAction(tr("&Copy IP/Netmask"), [this] { + GUIUtil::copyEntryData(ui->banlistWidget, BanTableModel::Address, Qt::DisplayRole); + }); + banTableContextMenu->addSeparator(); banTableContextMenu->addAction(tr("&Unban"), this, &RPCConsole::unbanSelectedNode); connect(ui->banlistWidget, &QTableView::customContextMenuRequested, this, &RPCConsole::showBanTableContextMenu); @@ -1129,7 +1136,7 @@ void RPCConsole::updateDetailWidget() } const auto stats = selected_peers.first().data(PeerTableModel::StatsRole).value<CNodeCombinedStats*>(); // update the detail ui with latest node information - QString peerAddrDetails(QString::fromStdString(stats->nodeStats.addrName) + " "); + QString peerAddrDetails(QString::fromStdString(stats->nodeStats.m_addr_name) + " "); peerAddrDetails += tr("(peer: %1)").arg(QString::number(stats->nodeStats.nodeid)); if (!stats->nodeStats.addrLocal.empty()) peerAddrDetails += "<br />" + tr("via %1").arg(QString::fromStdString(stats->nodeStats.addrLocal)); diff --git a/src/qt/walletframe.cpp b/src/qt/walletframe.cpp index 3d8bc0c7c5..30c29eb356 100644 --- a/src/qt/walletframe.cpp +++ b/src/qt/walletframe.cpp @@ -109,7 +109,8 @@ void WalletFrame::setCurrentWallet(WalletModel* wallet_model) walletView->updateGeometry(); walletStack->setCurrentWidget(walletView); - walletView->updateEncryptionStatus(); + + Q_EMIT currentWalletSet(); } void WalletFrame::removeWallet(WalletModel* wallet_model) diff --git a/src/qt/walletframe.h b/src/qt/walletframe.h index fe42293abc..cbf6af95ec 100644 --- a/src/qt/walletframe.h +++ b/src/qt/walletframe.h @@ -49,6 +49,7 @@ public: Q_SIGNALS: void createWalletButtonClicked(); void message(const QString& title, const QString& message, unsigned int style); + void currentWalletSet(); private: QStackedWidget *walletStack; diff --git a/src/qt/walletview.cpp b/src/qt/walletview.cpp index 2326af80b6..7e96e85c0c 100644 --- a/src/qt/walletview.cpp +++ b/src/qt/walletview.cpp @@ -118,10 +118,6 @@ void WalletView::setWalletModel(WalletModel *_walletModel) // Handle changes in encryption status connect(_walletModel, &WalletModel::encryptionStatusChanged, this, &WalletView::encryptionStatusChanged); - updateEncryptionStatus(); - - // update HD status - Q_EMIT hdEnabledStatusChanged(); // Balloon pop-up for new transaction connect(_walletModel->getTransactionTableModel(), &TransactionTableModel::rowsInserted, this, &WalletView::processNewTransaction); @@ -211,11 +207,6 @@ void WalletView::showOutOfSyncWarning(bool fShow) overviewPage->showOutOfSyncWarning(fShow); } -void WalletView::updateEncryptionStatus() -{ - Q_EMIT encryptionStatusChanged(); -} - void WalletView::encryptWallet() { if(!walletModel) @@ -224,7 +215,7 @@ void WalletView::encryptWallet() dlg.setModel(walletModel); dlg.exec(); - updateEncryptionStatus(); + Q_EMIT encryptionStatusChanged(); } void WalletView::backupWallet() diff --git a/src/qt/walletview.h b/src/qt/walletview.h index 5c42a9ffc0..bb6ad0f69e 100644 --- a/src/qt/walletview.h +++ b/src/qt/walletview.h @@ -103,9 +103,6 @@ public Q_SLOTS: /** Show used receiving addresses */ void usedReceivingAddresses(); - /** Re-emit encryption status signal */ - void updateEncryptionStatus(); - /** Show progress dialog e.g. for rescan */ void showProgress(const QString &title, int nProgress); @@ -117,8 +114,6 @@ Q_SIGNALS: void message(const QString &title, const QString &message, unsigned int style); /** Encryption status of wallet changed */ void encryptionStatusChanged(); - /** HD-Enabled status of wallet changed (only possible during startup) */ - void hdEnabledStatusChanged(); /** Notify that a new transaction appeared */ void incomingTransaction(const QString& date, int unit, const CAmount& amount, const QString& type, const QString& address, const QString& label, const QString& walletName); /** Notify that the out of sync warning icon has been pressed */ diff --git a/src/qt/winshutdownmonitor.h b/src/qt/winshutdownmonitor.h index 8edb98c744..bf399edcf3 100644 --- a/src/qt/winshutdownmonitor.h +++ b/src/qt/winshutdownmonitor.h @@ -17,7 +17,7 @@ class WinShutdownMonitor : public QAbstractNativeEventFilter { public: /** Implements QAbstractNativeEventFilter interface for processing Windows messages */ - bool nativeEventFilter(const QByteArray &eventType, void *pMessage, long *pnResult); + bool nativeEventFilter(const QByteArray &eventType, void *pMessage, long *pnResult) override; /** Register the reason for blocking shutdown on Windows to allow clean client exit */ static void registerShutdownBlockReason(const QString& strReason, const HWND& mainWinId); diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 692096367c..27cbb3a702 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -1089,7 +1089,8 @@ static RPCHelpMan estimatesmartfee() "have been observed to make an estimate for any number of blocks."}, }}, RPCExamples{ - HelpExampleCli("estimatesmartfee", "6") + HelpExampleCli("estimatesmartfee", "6") + + HelpExampleRpc("estimatesmartfee", "6") }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index 1a94abf6d3..e5804211f3 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -109,7 +109,7 @@ static RPCHelpMan createmultisig() "\nCreate a multisig address from 2 public keys\n" + HelpExampleCli("createmultisig", "2 \"[\\\"03789ed0bb717d88f7d321a368d905e7430207ebbd82bd342cf11ae157a7ace5fd\\\",\\\"03dbc6764b8884a92e871274b87583e6d5c2a58819473e17e107ef3f6aa5a61626\\\"]\"") + "\nAs a JSON-RPC call\n" - + HelpExampleRpc("createmultisig", "2, \"[\\\"03789ed0bb717d88f7d321a368d905e7430207ebbd82bd342cf11ae157a7ace5fd\\\",\\\"03dbc6764b8884a92e871274b87583e6d5c2a58819473e17e107ef3f6aa5a61626\\\"]\"") + + HelpExampleRpc("createmultisig", "2, [\"03789ed0bb717d88f7d321a368d905e7430207ebbd82bd342cf11ae157a7ace5fd\",\"03dbc6764b8884a92e871274b87583e6d5c2a58819473e17e107ef3f6aa5a61626\"]") }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { @@ -158,6 +158,8 @@ static RPCHelpMan createmultisig() static RPCHelpMan getdescriptorinfo() { + const std::string EXAMPLE_DESCRIPTOR = "wpkh([d34db33f/84h/0h/0h]0279be667ef9dcbbac55a06295Ce870b07029Bfcdb2dce28d959f2815b16f81798)"; + return RPCHelpMan{"getdescriptorinfo", {"\nAnalyses a descriptor.\n"}, { @@ -175,7 +177,8 @@ static RPCHelpMan getdescriptorinfo() }, RPCExamples{ "Analyse a descriptor\n" + - HelpExampleCli("getdescriptorinfo", "\"wpkh([d34db33f/84h/0h/0h]0279be667ef9dcbbac55a06295Ce870b07029Bfcdb2dce28d959f2815b16f81798)\"") + HelpExampleCli("getdescriptorinfo", "\"" + EXAMPLE_DESCRIPTOR + "\"") + + HelpExampleRpc("getdescriptorinfo", "\"" + EXAMPLE_DESCRIPTOR + "\"") }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { @@ -201,6 +204,8 @@ static RPCHelpMan getdescriptorinfo() static RPCHelpMan deriveaddresses() { + const std::string EXAMPLE_DESCRIPTOR = "wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/*)#cjjspncu"; + return RPCHelpMan{"deriveaddresses", {"\nDerives one or more addresses corresponding to an output descriptor.\n" "Examples of output descriptors are:\n" @@ -223,7 +228,8 @@ static RPCHelpMan deriveaddresses() }, RPCExamples{ "First three native segwit receive addresses\n" + - HelpExampleCli("deriveaddresses", "\"wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/*)#cjjspncu\" \"[0,2]\"") + HelpExampleCli("deriveaddresses", "\"" + EXAMPLE_DESCRIPTOR + "\" \"[0,2]\"") + + HelpExampleRpc("deriveaddresses", "\"" + EXAMPLE_DESCRIPTOR + "\", \"[0,2]\"") }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index 861b889118..0f554ec5e7 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -197,7 +197,7 @@ static RPCHelpMan getpeerinfo() CNodeStateStats statestats; bool fStateStats = peerman.GetNodeStateStats(stats.nodeid, statestats); obj.pushKV("id", stats.nodeid); - obj.pushKV("addr", stats.addrName); + obj.pushKV("addr", stats.m_addr_name); if (stats.addrBind.IsValid()) { obj.pushKV("addrbind", stats.addrBind.ToString()); } diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index 682b55742a..621a1b9fd6 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -1242,14 +1242,8 @@ std::unique_ptr<PubkeyProvider> InferXOnlyPubkey(const XOnlyPubKey& xkey, ParseS CPubKey pubkey(full_key); std::unique_ptr<PubkeyProvider> key_provider = std::make_unique<ConstPubkeyProvider>(0, pubkey, true); KeyOriginInfo info; - if (provider.GetKeyOrigin(pubkey.GetID(), info)) { + if (provider.GetKeyOriginByXOnly(xkey, info)) { return std::make_unique<OriginPubkeyProvider>(0, std::move(info), std::move(key_provider)); - } else { - full_key[0] = 0x03; - pubkey = CPubKey(full_key); - if (provider.GetKeyOrigin(pubkey.GetID(), info)) { - return std::make_unique<OriginPubkeyProvider>(0, std::move(info), std::move(key_provider)); - } } return key_provider; } diff --git a/src/script/script.h b/src/script/script.h index 974cde4984..8cd1cc3855 100644 --- a/src/script/script.h +++ b/src/script/script.h @@ -6,6 +6,7 @@ #ifndef BITCOIN_SCRIPT_SCRIPT_H #define BITCOIN_SCRIPT_SCRIPT_H +#include <attributes.h> #include <crypto/common.h> #include <prevector.h> #include <serialize.h> @@ -438,9 +439,9 @@ public: /** Delete non-existent operator to defend against future introduction */ CScript& operator<<(const CScript& b) = delete; - CScript& operator<<(int64_t b) { return push_int64(b); } + CScript& operator<<(int64_t b) LIFETIMEBOUND { return push_int64(b); } - CScript& operator<<(opcodetype opcode) + CScript& operator<<(opcodetype opcode) LIFETIMEBOUND { if (opcode < 0 || opcode > 0xff) throw std::runtime_error("CScript::operator<<(): invalid opcode"); @@ -448,13 +449,13 @@ public: return *this; } - CScript& operator<<(const CScriptNum& b) + CScript& operator<<(const CScriptNum& b) LIFETIMEBOUND { *this << b.getvch(); return *this; } - CScript& operator<<(const std::vector<unsigned char>& b) + CScript& operator<<(const std::vector<unsigned char>& b) LIFETIMEBOUND { if (b.size() < OP_PUSHDATA1) { diff --git a/src/script/sign.cpp b/src/script/sign.cpp index 4714d0ef11..b912b00365 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -60,22 +60,7 @@ bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider& assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT); CKey key; - { - // For now, use the old full pubkey-based key derivation logic. As it is indexed by - // Hash160(full pubkey), we need to try both a version prefixed with 0x02, and one - // with 0x03. - unsigned char b[33] = {0x02}; - std::copy(pubkey.begin(), pubkey.end(), b + 1); - CPubKey fullpubkey; - fullpubkey.Set(b, b + 33); - CKeyID keyid = fullpubkey.GetID(); - if (!provider.GetKey(keyid, key)) { - b[0] = 0x03; - fullpubkey.Set(b, b + 33); - CKeyID keyid = fullpubkey.GetID(); - if (!provider.GetKey(keyid, key)) return false; - } - } + if (!provider.GetKeyByXOnly(pubkey, key)) return false; // BIP341/BIP342 signing needs lots of precomputed transaction data. While some // (non-SIGHASH_DEFAULT) sighash modes exist that can work with just some subset diff --git a/src/script/signingprovider.h b/src/script/signingprovider.h index 939ae10622..fbce61c6a9 100644 --- a/src/script/signingprovider.h +++ b/src/script/signingprovider.h @@ -26,6 +26,30 @@ public: virtual bool HaveKey(const CKeyID &address) const { return false; } virtual bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const { return false; } virtual bool GetTaprootSpendData(const XOnlyPubKey& output_key, TaprootSpendData& spenddata) const { return false; } + + bool GetKeyByXOnly(const XOnlyPubKey& pubkey, CKey& key) const + { + for (const auto& id : pubkey.GetKeyIDs()) { + if (GetKey(id, key)) return true; + } + return false; + } + + bool GetPubKeyByXOnly(const XOnlyPubKey& pubkey, CPubKey& out) const + { + for (const auto& id : pubkey.GetKeyIDs()) { + if (GetPubKey(id, out)) return true; + } + return false; + } + + bool GetKeyOriginByXOnly(const XOnlyPubKey& pubkey, KeyOriginInfo& info) const + { + for (const auto& id : pubkey.GetKeyIDs()) { + if (GetKeyOrigin(id, info)) return true; + } + return false; + } }; extern const SigningProvider& DUMMY_SIGNING_PROVIDER; diff --git a/src/sync.cpp b/src/sync.cpp index a2b62c2286..eace86d9dd 100644 --- a/src/sync.cpp +++ b/src/sync.cpp @@ -9,6 +9,7 @@ #include <sync.h> #include <logging.h> +#include <logging/timer.h> #include <tinyformat.h> #include <util/strencodings.h> #include <util/threadnames.h> @@ -23,16 +24,10 @@ #include <utility> #include <vector> -#ifdef DEBUG_LOCKCONTENTION -#if !defined(HAVE_THREAD_LOCAL) -static_assert(false, "thread_local is not supported"); -#endif -void PrintLockContention(const char* pszName, const char* pszFile, int nLine) +void LockContention(const char* pszName, const char* pszFile, int nLine) { - LogPrintf("LOCKCONTENTION: %s\n", pszName); - LogPrintf("Locker: %s:%d\n", pszFile, nLine); + LOG_TIME_MICROS_WITH_CATEGORY(strprintf("%s, %s:%d", pszName, pszFile, nLine), BCLog::LOCK); } -#endif /* DEBUG_LOCKCONTENTION */ #ifdef DEBUG_LOCKORDER // diff --git a/src/sync.h b/src/sync.h index 146c228592..bf15c0b4eb 100644 --- a/src/sync.h +++ b/src/sync.h @@ -126,9 +126,8 @@ using RecursiveMutex = AnnotatedMixin<std::recursive_mutex>; /** Wrapped mutex: supports waiting but not recursive locking */ typedef AnnotatedMixin<std::mutex> Mutex; -#ifdef DEBUG_LOCKCONTENTION -void PrintLockContention(const char* pszName, const char* pszFile, int nLine); -#endif +/** Prints a lock contention to the log */ +void LockContention(const char* pszName, const char* pszFile, int nLine); /** Wrapper around std::unique_lock style lock for Mutex. */ template <typename Mutex, typename Base = typename Mutex::UniqueLock> @@ -138,22 +137,18 @@ private: void Enter(const char* pszName, const char* pszFile, int nLine) { EnterCritical(pszName, pszFile, nLine, Base::mutex()); -#ifdef DEBUG_LOCKCONTENTION - if (!Base::try_lock()) { - PrintLockContention(pszName, pszFile, nLine); -#endif - Base::lock(); -#ifdef DEBUG_LOCKCONTENTION - } -#endif + if (Base::try_lock()) return; + LockContention(pszName, pszFile, nLine); // log the contention + Base::lock(); } bool TryEnter(const char* pszName, const char* pszFile, int nLine) { EnterCritical(pszName, pszFile, nLine, Base::mutex(), true); Base::try_lock(); - if (!Base::owns_lock()) + if (!Base::owns_lock()) { LeaveCritical(); + } return Base::owns_lock(); } diff --git a/src/test/bip32_tests.cpp b/src/test/bip32_tests.cpp index fb16c92647..a89868e1ef 100644 --- a/src/test/bip32_tests.cpp +++ b/src/test/bip32_tests.cpp @@ -14,6 +14,8 @@ #include <string> #include <vector> +namespace { + struct TestDerivation { std::string pub; std::string prv; @@ -99,7 +101,26 @@ TestVector test4 = "xprv9xJocDuwtYCMNAo3Zw76WENQeAS6WGXQ55RCy7tDJ8oALr4FWkuVoHJeHVAcAqiZLE7Je3vZJHxspZdFHfnBEjHqU5hG1Jaj32dVoS6XLT1", 0); -static void RunTest(const TestVector &test) { +const std::vector<std::string> TEST5 = { + "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5fTtTQBm", + "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGTQQD3dC4H2D5GBj7vWvSQaaBv5cxi9gafk7NF3pnBju6dwKvH", + "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Txnt3siSujt9RCVYsx4qHZGc62TG4McvMGcAUjeuwZdduYEvFn", + "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGpWnsj83BHtEy5Zt8CcDr1UiRXuWCmTQLxEK9vbz5gPstX92JQ", + "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6N8ZMMXctdiCjxTNq964yKkwrkBJJwpzZS4HS2fxvyYUA4q2Xe4", + "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD9y5gkZ6Eq3Rjuahrv17fEQ3Qen6J", + "xprv9s2SPatNQ9Vc6GTbVMFPFo7jsaZySyzk7L8n2uqKXJen3KUmvQNTuLh3fhZMBoG3G4ZW1N2kZuHEPY53qmbZzCHshoQnNf4GvELZfqTUrcv", + "xpub661no6RGEX3uJkY4bNnPcw4URcQTrSibUZ4NqJEw5eBkv7ovTwgiT91XX27VbEXGENhYRCf7hyEbWrR3FewATdCEebj6znwMfQkhRYHRLpJ", + "xprv9s21ZrQH4r4TsiLvyLXqM9P7k1K3EYhA1kkD6xuquB5i39AU8KF42acDyL3qsDbU9NmZn6MsGSUYZEsuoePmjzsB3eFKSUEh3Gu1N3cqVUN", + "xpub661MyMwAuDcm6CRQ5N4qiHKrJ39Xe1R1NyfouMKTTWcguwVcfrZJaNvhpebzGerh7gucBvzEQWRugZDuDXjNDRmXzSZe4c7mnTK97pTvGS8", + "DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHGMQzT7ayAmfo4z3gY5KfbrZWZ6St24UVf2Qgo6oujFktLHdHY4", + "DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHPmHJiEDXkTiJTVV9rHEBUem2mwVbbNfvT2MTcAqj3nesx8uBf9", + "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzF93Y5wvzdUayhgkkFoicQZcP3y52uPPxFnfoLZB21Teqt1VvEHx", + "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD5SDKr24z3aiUvKr9bJpdrcLg1y3G", + "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Q5JXayek4PRsn35jii4veMimro1xefsM58PgBMrvdYre8QyULY", + "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHL" +}; + +void RunTest(const TestVector &test) { std::vector<unsigned char> seed = ParseHex(test.strHexMaster); CExtKey key; CExtPubKey pubkey; @@ -133,6 +154,8 @@ static void RunTest(const TestVector &test) { } } +} // namespace + BOOST_FIXTURE_TEST_SUITE(bip32_tests, BasicTestingSetup) BOOST_AUTO_TEST_CASE(bip32_test1) { @@ -151,4 +174,13 @@ BOOST_AUTO_TEST_CASE(bip32_test4) { RunTest(test4); } +BOOST_AUTO_TEST_CASE(bip32_test5) { + for (const auto& str : TEST5) { + auto dec_extkey = DecodeExtKey(str); + auto dec_extpubkey = DecodeExtPubKey(str); + BOOST_CHECK_MESSAGE(!dec_extkey.key.IsValid(), "Decoding '" + str + "' as xprv should fail"); + BOOST_CHECK_MESSAGE(!dec_extpubkey.pubkey.IsValid(), "Decoding '" + str + "' as xpub should fail"); + } +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/fuzz/addrdb.cpp b/src/test/fuzz/addrdb.cpp deleted file mode 100644 index d15c785673..0000000000 --- a/src/test/fuzz/addrdb.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2020 The Bitcoin Core developers -// Distributed under the MIT software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -#include <addrdb.h> -#include <test/fuzz/FuzzedDataProvider.h> -#include <test/fuzz/fuzz.h> -#include <test/fuzz/util.h> - -#include <cassert> -#include <cstdint> -#include <optional> -#include <string> -#include <vector> - -FUZZ_TARGET(addrdb) -{ - FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); - - // The point of this code is to exercise all CBanEntry constructors. - const CBanEntry ban_entry = [&] { - switch (fuzzed_data_provider.ConsumeIntegralInRange<int>(0, 2)) { - case 0: - return CBanEntry{fuzzed_data_provider.ConsumeIntegral<int64_t>()}; - break; - case 1: { - const std::optional<CBanEntry> ban_entry = ConsumeDeserializable<CBanEntry>(fuzzed_data_provider); - if (ban_entry) { - return *ban_entry; - } - break; - } - } - return CBanEntry{}; - }(); - (void)ban_entry; // currently unused -} diff --git a/src/test/fuzz/blockfilter.cpp b/src/test/fuzz/blockfilter.cpp index 7fa06085f8..96f049625d 100644 --- a/src/test/fuzz/blockfilter.cpp +++ b/src/test/fuzz/blockfilter.cpp @@ -36,9 +36,10 @@ FUZZ_TARGET(blockfilter) (void)gcs_filter.GetEncoded(); (void)gcs_filter.Match(ConsumeRandomLengthByteVector(fuzzed_data_provider)); GCSFilter::ElementSet element_set; - while (fuzzed_data_provider.ConsumeBool()) { + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 30000) + { element_set.insert(ConsumeRandomLengthByteVector(fuzzed_data_provider)); - gcs_filter.MatchAny(element_set); } + gcs_filter.MatchAny(element_set); } } diff --git a/src/test/fuzz/deserialize.cpp b/src/test/fuzz/deserialize.cpp index f4235a973a..83ae1680e3 100644 --- a/src/test/fuzz/deserialize.cpp +++ b/src/test/fuzz/deserialize.cpp @@ -195,10 +195,6 @@ FUZZ_TARGET_DESERIALIZE(blockheader_deserialize, { CBlockHeader bh; DeserializeFromFuzzingInput(buffer, bh); }) -FUZZ_TARGET_DESERIALIZE(banentry_deserialize, { - CBanEntry be; - DeserializeFromFuzzingInput(buffer, be); -}) FUZZ_TARGET_DESERIALIZE(txundo_deserialize, { CTxUndo tu; DeserializeFromFuzzingInput(buffer, tu); diff --git a/src/test/fuzz/net.cpp b/src/test/fuzz/net.cpp index e00b5b09bf..ff0259c182 100644 --- a/src/test/fuzz/net.cpp +++ b/src/test/fuzz/net.cpp @@ -38,9 +38,6 @@ FUZZ_TARGET_INIT(net, initialize_net) node.CloseSocketDisconnect(); }, [&] { - node.MaybeSetAddrName(fuzzed_data_provider.ConsumeRandomLengthString(32)); - }, - [&] { const std::vector<bool> asmap = ConsumeRandomLengthBitVector(fuzzed_data_provider); if (!SanityCheckASMap(asmap)) { return; @@ -82,7 +79,6 @@ FUZZ_TARGET_INIT(net, initialize_net) } (void)node.GetAddrLocal(); - (void)node.GetAddrName(); (void)node.GetId(); (void)node.GetLocalNonce(); (void)node.GetLocalServices(); diff --git a/src/test/logging_tests.cpp b/src/test/logging_tests.cpp index e99c6e0fc8..e2e31c62d7 100644 --- a/src/test/logging_tests.cpp +++ b/src/test/logging_tests.cpp @@ -27,7 +27,7 @@ BOOST_AUTO_TEST_CASE(logging_timer) SetMockTime(1); auto micro_timer = BCLog::Timer<std::chrono::microseconds>("tests", "end_msg"); SetMockTime(2); - BOOST_CHECK_EQUAL(micro_timer.LogMsg("test micros"), "tests: test micros (1000000.00μs)"); + BOOST_CHECK_EQUAL(micro_timer.LogMsg("test micros"), "tests: test micros (1000000μs)"); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 803a7b8b15..29938d4ede 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -627,37 +627,42 @@ BOOST_AUTO_TEST_CASE(ipv4_peer_with_ipv6_addrMe_test) BOOST_AUTO_TEST_CASE(LimitedAndReachable_Network) { - BOOST_CHECK_EQUAL(IsReachable(NET_IPV4), true); - BOOST_CHECK_EQUAL(IsReachable(NET_IPV6), true); - BOOST_CHECK_EQUAL(IsReachable(NET_ONION), true); + BOOST_CHECK(IsReachable(NET_IPV4)); + BOOST_CHECK(IsReachable(NET_IPV6)); + BOOST_CHECK(IsReachable(NET_ONION)); + BOOST_CHECK(IsReachable(NET_I2P)); SetReachable(NET_IPV4, false); SetReachable(NET_IPV6, false); SetReachable(NET_ONION, false); + SetReachable(NET_I2P, false); - BOOST_CHECK_EQUAL(IsReachable(NET_IPV4), false); - BOOST_CHECK_EQUAL(IsReachable(NET_IPV6), false); - BOOST_CHECK_EQUAL(IsReachable(NET_ONION), false); + BOOST_CHECK(!IsReachable(NET_IPV4)); + BOOST_CHECK(!IsReachable(NET_IPV6)); + BOOST_CHECK(!IsReachable(NET_ONION)); + BOOST_CHECK(!IsReachable(NET_I2P)); SetReachable(NET_IPV4, true); SetReachable(NET_IPV6, true); SetReachable(NET_ONION, true); + SetReachable(NET_I2P, true); - BOOST_CHECK_EQUAL(IsReachable(NET_IPV4), true); - BOOST_CHECK_EQUAL(IsReachable(NET_IPV6), true); - BOOST_CHECK_EQUAL(IsReachable(NET_ONION), true); + BOOST_CHECK(IsReachable(NET_IPV4)); + BOOST_CHECK(IsReachable(NET_IPV6)); + BOOST_CHECK(IsReachable(NET_ONION)); + BOOST_CHECK(IsReachable(NET_I2P)); } BOOST_AUTO_TEST_CASE(LimitedAndReachable_NetworkCaseUnroutableAndInternal) { - BOOST_CHECK_EQUAL(IsReachable(NET_UNROUTABLE), true); - BOOST_CHECK_EQUAL(IsReachable(NET_INTERNAL), true); + BOOST_CHECK(IsReachable(NET_UNROUTABLE)); + BOOST_CHECK(IsReachable(NET_INTERNAL)); SetReachable(NET_UNROUTABLE, false); SetReachable(NET_INTERNAL, false); - BOOST_CHECK_EQUAL(IsReachable(NET_UNROUTABLE), true); // Ignored for both networks - BOOST_CHECK_EQUAL(IsReachable(NET_INTERNAL), true); + BOOST_CHECK(IsReachable(NET_UNROUTABLE)); // Ignored for both networks + BOOST_CHECK(IsReachable(NET_INTERNAL)); } CNetAddr UtilBuildAddress(unsigned char p1, unsigned char p2, unsigned char p3, unsigned char p4) @@ -676,10 +681,10 @@ BOOST_AUTO_TEST_CASE(LimitedAndReachable_CNetAddr) CNetAddr addr = UtilBuildAddress(0x001, 0x001, 0x001, 0x001); // 1.1.1.1 SetReachable(NET_IPV4, true); - BOOST_CHECK_EQUAL(IsReachable(addr), true); + BOOST_CHECK(IsReachable(addr)); SetReachable(NET_IPV4, false); - BOOST_CHECK_EQUAL(IsReachable(addr), false); + BOOST_CHECK(!IsReachable(addr)); SetReachable(NET_IPV4, true); // have to reset this, because this is stateful. } @@ -691,12 +696,12 @@ BOOST_AUTO_TEST_CASE(LocalAddress_BasicLifecycle) SetReachable(NET_IPV4, true); - BOOST_CHECK_EQUAL(IsLocal(addr), false); - BOOST_CHECK_EQUAL(AddLocal(addr, 1000), true); - BOOST_CHECK_EQUAL(IsLocal(addr), true); + BOOST_CHECK(!IsLocal(addr)); + BOOST_CHECK(AddLocal(addr, 1000)); + BOOST_CHECK(IsLocal(addr)); RemoveLocal(addr); - BOOST_CHECK_EQUAL(IsLocal(addr), false); + BOOST_CHECK(!IsLocal(addr)); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp index 97fd0600fa..24029ea02e 100644 --- a/src/test/transaction_tests.cpp +++ b/src/test/transaction_tests.cpp @@ -765,95 +765,89 @@ BOOST_AUTO_TEST_CASE(test_IsStandard) key.MakeNewKey(true); t.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey())); - std::string reason; - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + constexpr auto CheckIsStandard = [](const auto& t) { + std::string reason; + BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + BOOST_CHECK(reason.empty()); + }; + constexpr auto CheckIsNotStandard = [](const auto& t, const std::string& reason_in) { + std::string reason; + BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); + BOOST_CHECK_EQUAL(reason_in, reason); + }; + + CheckIsStandard(t); // Check dust with default relay fee: - CAmount nDustThreshold = 182 * dustRelayFee.GetFeePerK()/1000; + CAmount nDustThreshold = 182 * dustRelayFee.GetFeePerK() / 1000; BOOST_CHECK_EQUAL(nDustThreshold, 546); // dust: t.vout[0].nValue = nDustThreshold - 1; - reason.clear(); - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "dust"); + CheckIsNotStandard(t, "dust"); // not dust: t.vout[0].nValue = nDustThreshold; - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); // Disallowed nVersion t.nVersion = -1; - reason.clear(); - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "version"); + CheckIsNotStandard(t, "version"); t.nVersion = 0; - reason.clear(); - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "version"); + CheckIsNotStandard(t, "version"); t.nVersion = 3; - reason.clear(); - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "version"); + CheckIsNotStandard(t, "version"); // Allowed nVersion t.nVersion = 1; - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); t.nVersion = 2; - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); // Check dust with odd relay fee to verify rounding: // nDustThreshold = 182 * 3702 / 1000 dustRelayFee = CFeeRate(3702); // dust: t.vout[0].nValue = 673 - 1; - reason.clear(); - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "dust"); + CheckIsNotStandard(t, "dust"); // not dust: t.vout[0].nValue = 673; - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); dustRelayFee = CFeeRate(DUST_RELAY_TX_FEE); t.vout[0].scriptPubKey = CScript() << OP_1; - reason.clear(); - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "scriptpubkey"); + CheckIsNotStandard(t, "scriptpubkey"); // MAX_OP_RETURN_RELAY-byte TxoutType::NULL_DATA (standard) t.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"); BOOST_CHECK_EQUAL(MAX_OP_RETURN_RELAY, t.vout[0].scriptPubKey.size()); - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); // MAX_OP_RETURN_RELAY+1-byte TxoutType::NULL_DATA (non-standard) t.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3800"); BOOST_CHECK_EQUAL(MAX_OP_RETURN_RELAY + 1, t.vout[0].scriptPubKey.size()); - reason.clear(); - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "scriptpubkey"); + CheckIsNotStandard(t, "scriptpubkey"); // Data payload can be encoded in any way... t.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex(""); - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); t.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("00") << ParseHex("01"); - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); // OP_RESERVED *is* considered to be a PUSHDATA type opcode by IsPushOnly()! t.vout[0].scriptPubKey = CScript() << OP_RETURN << OP_RESERVED << -1 << 0 << ParseHex("01") << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10 << 11 << 12 << 13 << 14 << 15 << 16; - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); t.vout[0].scriptPubKey = CScript() << OP_RETURN << 0 << ParseHex("01") << 2 << ParseHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); // ...so long as it only contains PUSHDATA's t.vout[0].scriptPubKey = CScript() << OP_RETURN << OP_RETURN; - reason.clear(); - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "scriptpubkey"); + CheckIsNotStandard(t, "scriptpubkey"); // TxoutType::NULL_DATA w/o PUSHDATA t.vout.resize(1); t.vout[0].scriptPubKey = CScript() << OP_RETURN; - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); // Only one TxoutType::NULL_DATA permitted in all cases t.vout.resize(2); @@ -861,21 +855,15 @@ BOOST_AUTO_TEST_CASE(test_IsStandard) t.vout[0].nValue = 0; t.vout[1].scriptPubKey = CScript() << OP_RETURN << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"); t.vout[1].nValue = 0; - reason.clear(); - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "multi-op-return"); + CheckIsNotStandard(t, "multi-op-return"); t.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"); t.vout[1].scriptPubKey = CScript() << OP_RETURN; - reason.clear(); - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "multi-op-return"); + CheckIsNotStandard(t, "multi-op-return"); t.vout[0].scriptPubKey = CScript() << OP_RETURN; t.vout[1].scriptPubKey = CScript() << OP_RETURN; - reason.clear(); - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "multi-op-return"); + CheckIsNotStandard(t, "multi-op-return"); // Check large scriptSig (non-standard if size is >1650 bytes) t.vout.resize(1); @@ -883,12 +871,10 @@ BOOST_AUTO_TEST_CASE(test_IsStandard) t.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey())); // OP_PUSHDATA2 with len (3 bytes) + data (1647 bytes) = 1650 bytes t.vin[0].scriptSig = CScript() << std::vector<unsigned char>(1647, 0); // 1650 - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); t.vin[0].scriptSig = CScript() << std::vector<unsigned char>(1648, 0); // 1651 - reason.clear(); - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "scriptsig-size"); + CheckIsNotStandard(t, "scriptsig-size"); // Check scriptSig format (non-standard if there are any other ops than just PUSHs) t.vin[0].scriptSig = CScript() @@ -897,7 +883,7 @@ BOOST_AUTO_TEST_CASE(test_IsStandard) << std::vector<unsigned char>(235, 0) // OP_PUSHDATA1 x [...x bytes...] << std::vector<unsigned char>(1234, 0) // OP_PUSHDATA2 x [...x bytes...] << OP_9; - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); const std::vector<unsigned char> non_push_ops = { // arbitrary set of non-push operations OP_NOP, OP_VERIFY, OP_IF, OP_ROT, OP_3DUP, OP_SIZE, OP_EQUAL, OP_ADD, OP_SUB, @@ -917,11 +903,10 @@ BOOST_AUTO_TEST_CASE(test_IsStandard) // replace current push-op with each non-push-op for (auto op : non_push_ops) { t.vin[0].scriptSig[index] = op; - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "scriptsig-not-pushonly"); + CheckIsNotStandard(t, "scriptsig-not-pushonly"); } t.vin[0].scriptSig[index] = orig_op; // restore op - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); } // Check tx-size (non-standard if transaction weight is > MAX_STANDARD_TX_WEIGHT) @@ -934,27 +919,47 @@ BOOST_AUTO_TEST_CASE(test_IsStandard) // =============================== // total: 400000 vbytes BOOST_CHECK_EQUAL(GetTransactionWeight(CTransaction(t)), 400000); - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); // increase output size by one byte, so we end up with 400004 vbytes t.vout[0].scriptPubKey = CScript() << OP_RETURN << std::vector<unsigned char>(20, 0); // output size: 31 bytes BOOST_CHECK_EQUAL(GetTransactionWeight(CTransaction(t)), 400004); - reason.clear(); - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "tx-size"); + CheckIsNotStandard(t, "tx-size"); // Check bare multisig (standard if policy flag fIsBareMultisigStd is set) fIsBareMultisigStd = true; t.vout[0].scriptPubKey = GetScriptForMultisig(1, {key.GetPubKey()}); // simple 1-of-1 t.vin.resize(1); t.vin[0].scriptSig = CScript() << std::vector<unsigned char>(65, 0); - BOOST_CHECK(IsStandardTx(CTransaction(t), reason)); + CheckIsStandard(t); fIsBareMultisigStd = false; - reason.clear(); - BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); - BOOST_CHECK_EQUAL(reason, "bare-multisig"); + CheckIsNotStandard(t, "bare-multisig"); fIsBareMultisigStd = DEFAULT_PERMIT_BAREMULTISIG; + + // Check P2WPKH outputs dust threshold + t.vout[0].scriptPubKey = CScript() << OP_0 << ParseHex("ffffffffffffffffffffffffffffffffffffffff"); + t.vout[0].nValue = 294; + CheckIsStandard(t); + t.vout[0].nValue = 293; + CheckIsNotStandard(t, "dust"); + + // Check P2WSH outputs dust threshold + t.vout[0].scriptPubKey = CScript() << OP_0 << ParseHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + t.vout[0].nValue = 330; + CheckIsStandard(t); + t.vout[0].nValue = 329; + CheckIsNotStandard(t, "dust"); + + // Check future Witness Program versions dust threshold + for (int op = OP_2; op <= OP_16; op += 1) { + t.vout[0].scriptPubKey = CScript() << (opcodetype)op << ParseHex("ffff"); + t.vout[0].nValue = 240; + CheckIsStandard(t); + + t.vout[0].nValue = 239; + CheckIsNotStandard(t, "dust"); + } } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index 4f9d10cf1d..a62abf9b9c 100644 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -173,6 +173,22 @@ BOOST_AUTO_TEST_CASE(util_Join) BOOST_CHECK_EQUAL(Join<std::string>({"foo", "bar"}, ", ", op_upper), "FOO, BAR"); } +BOOST_AUTO_TEST_CASE(util_TrimString) +{ + BOOST_CHECK_EQUAL(TrimString(" foo bar "), "foo bar"); + BOOST_CHECK_EQUAL(TrimString("\t \n \n \f\n\r\t\v\tfoo \n \f\n\r\t\v\tbar\t \n \f\n\r\t\v\t\n "), "foo \n \f\n\r\t\v\tbar"); + BOOST_CHECK_EQUAL(TrimString("\t \n foo \n\tbar\t \n "), "foo \n\tbar"); + BOOST_CHECK_EQUAL(TrimString("\t \n foo \n\tbar\t \n ", "fobar"), "\t \n foo \n\tbar\t \n "); + BOOST_CHECK_EQUAL(TrimString("foo bar"), "foo bar"); + BOOST_CHECK_EQUAL(TrimString("foo bar", "fobar"), " "); + BOOST_CHECK_EQUAL(TrimString(std::string("\0 foo \0 ", 8)), std::string("\0 foo \0", 7)); + BOOST_CHECK_EQUAL(TrimString(std::string(" foo ", 5)), std::string("foo", 3)); + BOOST_CHECK_EQUAL(TrimString(std::string("\t\t\0\0\n\n", 6)), std::string("\0\0", 2)); + BOOST_CHECK_EQUAL(TrimString(std::string("\x05\x04\x03\x02\x01\x00", 6)), std::string("\x05\x04\x03\x02\x01\x00", 6)); + BOOST_CHECK_EQUAL(TrimString(std::string("\x05\x04\x03\x02\x01\x00", 6), std::string("\x05\x04\x03\x02\x01", 5)), std::string("\0", 1)); + BOOST_CHECK_EQUAL(TrimString(std::string("\x05\x04\x03\x02\x01\x00", 6), std::string("\x05\x04\x03\x02\x01\x00", 6)), ""); +} + BOOST_AUTO_TEST_CASE(util_FormatParseISO8601DateTime) { BOOST_CHECK_EQUAL(FormatISO8601DateTime(1317425777), "2011-09-30T23:36:17Z"); diff --git a/src/util/rbf.h b/src/util/rbf.h index 6a20b37de5..4eb44b904f 100644 --- a/src/util/rbf.h +++ b/src/util/rbf.h @@ -11,8 +11,15 @@ class CTransaction; static const uint32_t MAX_BIP125_RBF_SEQUENCE = 0xfffffffd; -// Check whether the sequence numbers on this transaction are signaling -// opt-in to replace-by-fee, according to BIP 125 +/** Check whether the sequence numbers on this transaction are signaling +* opt-in to replace-by-fee, according to BIP 125. +* Allow opt-out of transaction replacement by setting +* nSequence > MAX_BIP125_RBF_SEQUENCE (SEQUENCE_FINAL-2) on all inputs. +* +* SEQUENCE_FINAL-1 is picked to still allow use of nLockTime by +* non-replaceable transactions. All inputs rather than just one +* is for the sake of multi-party protocols, where we don't +* want a single party to be able to disable replacement. */ bool SignalsOptInRBF(const CTransaction &tx); #endif // BITCOIN_UTIL_RBF_H diff --git a/src/util/settings.cpp b/src/util/settings.cpp index b92b1d30c3..846b34089d 100644 --- a/src/util/settings.cpp +++ b/src/util/settings.cpp @@ -60,9 +60,15 @@ bool ReadSettings(const fs::path& path, std::map<std::string, SettingsValue>& va values.clear(); errors.clear(); + // Ok for file to not exist + if (!fs::exists(path)) return true; + fsbridge::ifstream file; file.open(path); - if (!file.is_open()) return true; // Ok for file not to exist. + if (!file.is_open()) { + errors.emplace_back(strprintf("%s. Please check permissions.", path.string())); + return false; + } SettingsValue in; if (!in.read(std::string{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()})) { diff --git a/src/validation.cpp b/src/validation.cpp index ec457da5cc..753b824167 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -25,6 +25,7 @@ #include <node/coinstats.h> #include <node/ui_interface.h> #include <policy/policy.h> +#include <policy/rbf.h> #include <policy/settings.h> #include <pow.h> #include <primitives/block.h> @@ -474,8 +475,10 @@ private: bool m_replacement_transaction; CAmount m_base_fees; CAmount m_modified_fees; - CAmount m_conflicting_fees; - size_t m_conflicting_size; + /** Total modified fees of all transactions being replaced. */ + CAmount m_conflicting_fees{0}; + /** Total virtual size of all transactions being replaced. */ + size_t m_conflicting_size{0}; const CTransactionRef& m_ptx; const uint256& m_hash; @@ -602,14 +605,6 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) } if (!setConflicts.count(ptxConflicting->GetHash())) { - // Allow opt-out of transaction replacement by setting - // nSequence > MAX_BIP125_RBF_SEQUENCE (SEQUENCE_FINAL-2) on all inputs. - // - // SEQUENCE_FINAL-1 is picked to still allow use of nLockTime by - // non-replaceable transactions. All inputs rather than just one - // is for the sake of multi-party protocols, where we don't - // want a single party to be able to disable replacement. - // // Transactions that don't explicitly signal replaceability are // *not* replaceable with the current logic, even if one of their // unconfirmed ancestors signals replaceability. This diverges @@ -617,16 +612,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) // Applications relying on first-seen mempool behavior should // check all unconfirmed ancestors; otherwise an opt-in ancestor // might be replaced, causing removal of this descendant. - bool fReplacementOptOut = true; - for (const CTxIn &_txin : ptxConflicting->vin) - { - if (_txin.nSequence <= MAX_BIP125_RBF_SEQUENCE) - { - fReplacementOptOut = false; - break; - } - } - if (fReplacementOptOut) { + if (!SignalsOptInRBF(*ptxConflicting)) { return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "txn-mempool-conflict"); } @@ -796,11 +782,6 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) } } - // Check if it's economically rational to mine this transaction rather - // than the ones it replaces. - nConflictingFees = 0; - nConflictingSize = 0; - uint64_t nConflictingCount = 0; // If we don't hold the lock allConflicting might be incomplete; the // subsequent RemoveStaged() and addUnchecked() calls don't guarantee @@ -808,9 +789,8 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) fReplacementTransaction = setConflicts.size(); if (fReplacementTransaction) { + std::string err_string; CFeeRate newFeeRate(nModifiedFees, nSize); - std::set<uint256> setConflictsParents; - const int maxDescendantsToVisit = 100; for (const auto& mi : setIterConflicting) { // Don't allow the replacement to reduce the feerate of the // mempool. @@ -835,33 +815,26 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) newFeeRate.ToString(), oldFeeRate.ToString())); } + } + + // Calculate all conflicting entries and enforce Rule #5. + if (!GetEntriesForConflicts(tx, m_pool, setIterConflicting, allConflicting, err_string)) { + return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "too many potential replacements", err_string); + } + + // Check if it's economically rational to mine this transaction rather + // than the ones it replaces. + for (CTxMemPool::txiter it : allConflicting) { + nConflictingFees += it->GetModifiedFee(); + nConflictingSize += it->GetTxSize(); + } + std::set<uint256> setConflictsParents; + for (const auto& mi : setIterConflicting) { for (const CTxIn &txin : mi->GetTx().vin) { setConflictsParents.insert(txin.prevout.hash); } - - nConflictingCount += mi->GetCountWithDescendants(); - } - // This potentially overestimates the number of actual descendants - // but we just want to be conservative to avoid doing too much - // work. - if (nConflictingCount <= maxDescendantsToVisit) { - // If not too many to replace, then calculate the set of - // transactions that would have to be evicted - for (CTxMemPool::txiter it : setIterConflicting) { - m_pool.CalculateDescendants(it, allConflicting); - } - for (CTxMemPool::txiter it : allConflicting) { - nConflictingFees += it->GetModifiedFee(); - nConflictingSize += it->GetTxSize(); - } - } else { - return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "too many potential replacements", - strprintf("rejecting replacement %s; too many potential replacements (%d > %d)\n", - hash.ToString(), - nConflictingCount, - maxDescendantsToVisit)); } for (unsigned int j = 0; j < tx.vin.size(); j++) @@ -2970,10 +2943,7 @@ void CChainState::ReceivedBlockTransactions(const CBlock& block, CBlockIndex* pi CBlockIndex *pindex = queue.front(); queue.pop_front(); pindex->nChainTx = (pindex->pprev ? pindex->pprev->nChainTx : 0) + pindex->nTx; - { - LOCK(cs_nBlockSequenceId); - pindex->nSequenceId = nBlockSequenceId++; - } + pindex->nSequenceId = nBlockSequenceId++; if (m_chain.Tip() == nullptr || !setBlockIndexCandidates.value_comp()(pindex, m_chain.Tip())) { setBlockIndexCandidates.insert(pindex); } diff --git a/src/validation.h b/src/validation.h index b80fa9d328..d4fcac1d48 100644 --- a/src/validation.h +++ b/src/validation.h @@ -558,9 +558,8 @@ protected: * Every received block is assigned a unique and increasing identifier, so we * know which one to give priority in case of a fork. */ - RecursiveMutex cs_nBlockSequenceId; /** Blocks loaded from disk are assigned id 0, so start the counter at 1. */ - int32_t nBlockSequenceId = 1; + int32_t nBlockSequenceId GUARDED_BY(::cs_main) = 1; /** Decreasing counter (used by subsequent preciousblock calls). */ int32_t nBlockReverseSequenceId = -1; /** chainwork for the last block that preciousblock has been applied to. */ @@ -749,7 +748,7 @@ public: void PruneBlockIndexCandidates(); - void UnloadBlockIndex(); + void UnloadBlockIndex() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); /** Check whether we are doing an initial block download (synchronizing from disk or network) */ bool IsInitialBlockDownload() const; @@ -930,7 +929,7 @@ public: CChainState& InitializeChainstate( CTxMemPool* mempool, const std::optional<uint256>& snapshot_blockhash = std::nullopt) - EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + LIFETIMEBOUND EXCLUSIVE_LOCKS_REQUIRED(::cs_main); //! Get all chainstates currently being used. std::vector<CChainState*> GetAll(); diff --git a/src/wallet/coinselection.cpp b/src/wallet/coinselection.cpp index 25b1ee07e4..1699424657 100644 --- a/src/wallet/coinselection.cpp +++ b/src/wallet/coinselection.cpp @@ -341,3 +341,30 @@ CAmount OutputGroup::GetSelectionAmount() const { return m_subtract_fee_outputs ? m_value : effective_value; } + +CAmount GetSelectionWaste(const std::set<CInputCoin>& inputs, CAmount change_cost, CAmount target, bool use_effective_value) +{ + // This function should not be called with empty inputs as that would mean the selection failed + assert(!inputs.empty()); + + // Always consider the cost of spending an input now vs in the future. + CAmount waste = 0; + CAmount selected_effective_value = 0; + for (const CInputCoin& coin : inputs) { + waste += coin.m_fee - coin.m_long_term_fee; + selected_effective_value += use_effective_value ? coin.effective_value : coin.txout.nValue; + } + + if (change_cost) { + // Consider the cost of making change and spending it in the future + // If we aren't making change, the caller should've set change_cost to 0 + assert(change_cost > 0); + waste += change_cost; + } else { + // When we are not making change (change_cost == 0), consider the excess we are throwing away to fees + assert(selected_effective_value >= target); + waste += selected_effective_value - target; + } + + return waste; +} diff --git a/src/wallet/coinselection.h b/src/wallet/coinselection.h index 7a3fb82139..35617d455b 100644 --- a/src/wallet/coinselection.h +++ b/src/wallet/coinselection.h @@ -166,6 +166,21 @@ struct OutputGroup CAmount GetSelectionAmount() const; }; +/** Compute the waste for this result given the cost of change + * and the opportunity cost of spending these inputs now vs in the future. + * If change exists, waste = change_cost + inputs * (effective_feerate - long_term_feerate) + * If no change, waste = excess + inputs * (effective_feerate - long_term_feerate) + * where excess = selected_effective_value - target + * change_cost = effective_feerate * change_output_size + long_term_feerate * change_spend_size + * + * @param[in] inputs The selected inputs + * @param[in] change_cost The cost of creating change and spending it in the future. Only used if there is change. Must be 0 if there is no change. + * @param[in] target The amount targeted by the coin selection algorithm. + * @param[in] use_effective_value Whether to use the input's effective value (when true) or the real value (when false). + * @return The waste + */ +[[nodiscard]] CAmount GetSelectionWaste(const std::set<CInputCoin>& inputs, CAmount change_cost, CAmount target, bool use_effective_value = true); + 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 diff --git a/src/wallet/feebumper.cpp b/src/wallet/feebumper.cpp index 30fef50c3b..f2de68295e 100644 --- a/src/wallet/feebumper.cpp +++ b/src/wallet/feebumper.cpp @@ -12,6 +12,8 @@ #include <wallet/coincontrol.h> #include <wallet/feebumper.h> #include <wallet/fees.h> +#include <wallet/receive.h> +#include <wallet/spend.h> #include <wallet/wallet.h> //! Check whether transaction has descendant in wallet or mempool, or has been @@ -30,7 +32,7 @@ static feebumper::Result PreconditionChecks(const CWallet& wallet, const CWallet } } - if (wtx.GetDepthInMainChain() != 0) { + if (wallet.GetTxDepthInMainChain(wtx) != 0) { errors.push_back(Untranslated("Transaction has been mined, or is conflicted with a mined transaction")); return feebumper::Result::WALLET_ERROR; } @@ -48,7 +50,7 @@ static feebumper::Result PreconditionChecks(const CWallet& wallet, const CWallet // 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)) { + if (!AllInputsMine(wallet, *wtx.tx, filter)) { errors.push_back(Untranslated("Transaction contains inputs that don't belong to this wallet")); return feebumper::Result::WALLET_ERROR; } @@ -81,7 +83,7 @@ static feebumper::Result CheckFeeRate(const CWallet& wallet, const CWalletTx& wt // 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(); + CAmount old_fee = CachedTxGetDebit(wallet, wtx, filter) - wtx.tx->GetValueOut(); const int64_t txSize = GetVirtualTransactionSize(*(wtx.tx)); CFeeRate nOldFeeRate(old_fee, txSize); // Min total fee is old fee + relay fee @@ -174,7 +176,7 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo // 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)) { + if (!OutputIsChange(wallet, output)) { CRecipient recipient = {output.scriptPubKey, output.nValue, false}; recipients.push_back(recipient); } else { @@ -185,7 +187,7 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo } isminefilter filter = wallet.GetLegacyScriptPubKeyMan() && wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) ? ISMINE_WATCH_ONLY : ISMINE_SPENDABLE; - old_fee = wtx.GetDebit(filter) - wtx.tx->GetValueOut(); + old_fee = CachedTxGetDebit(wallet, wtx, filter) - wtx.tx->GetValueOut(); if (coin_control.m_feerate) { // The user provided a feeRate argument. @@ -220,7 +222,7 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo 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)) { + if (!CreateTransaction(wallet, 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; } diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp index eb0d6316c0..bb5f0cceff 100644 --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -45,6 +45,7 @@ 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("-consolidatefeerate=<amt>", strprintf("The maximum feerate (in %s/kvB) at which transaction building may use more inputs than strictly necessary so that the wallet's UTXO pool can be reduced (default: %s).", CURRENCY_UNIT, FormatMoney(DEFAULT_CONSOLIDATE_FEERATE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); 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", diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 0d4b98ecaf..9a8c1e3c02 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -23,7 +23,9 @@ #include <wallet/fees.h> #include <wallet/ismine.h> #include <wallet/load.h> +#include <wallet/receive.h> #include <wallet/rpcwallet.h> +#include <wallet/spend.h> #include <wallet/wallet.h> #include <memory> @@ -55,7 +57,7 @@ WalletTx MakeWalletTx(CWallet& wallet, const CWalletTx& wtx) 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.txin_is_mine.emplace_back(InputIsMine(wallet, txin)); } result.txout_is_mine.reserve(wtx.tx->vout.size()); result.txout_address.reserve(wtx.tx->vout.size()); @@ -67,9 +69,9 @@ WalletTx MakeWalletTx(CWallet& wallet, const CWalletTx& wtx) 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.credit = CachedTxGetCredit(wallet, wtx, ISMINE_ALL); + result.debit = CachedTxGetDebit(wallet, wtx, ISMINE_ALL); + result.change = CachedTxGetChange(wallet, wtx); result.time = wtx.GetTxTime(); result.value_map = wtx.mapValue; result.is_coinbase = wtx.IsCoinBase(); @@ -81,15 +83,15 @@ 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.blocks_to_maturity = wallet.GetTxBlocksToMaturity(wtx); + result.depth_in_main_chain = wallet.GetTxDepthInMainChain(wtx); 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_trusted = CachedTxIsTrusted(wallet, wtx); result.is_abandoned = wtx.isAbandoned(); result.is_coinbase = wtx.IsCoinBase(); - result.is_in_main_chain = wtx.IsInMainChain(); + result.is_in_main_chain = wallet.IsTxInMainChain(wtx); return result; } @@ -242,7 +244,7 @@ public: LOCK(m_wallet->cs_wallet); CTransactionRef tx; FeeCalculation fee_calc_out; - if (!m_wallet->CreateTransaction(recipients, tx, fee, change_pos, + if (!CreateTransaction(*m_wallet, recipients, tx, fee, change_pos, fail_reason, coin_control, fee_calc_out, sign)) { return {}; } @@ -358,7 +360,7 @@ public: } WalletBalances getBalances() override { - const auto bal = m_wallet->GetBalance(); + const auto bal = GetBalance(*m_wallet); WalletBalances result; result.balance = bal.m_mine_trusted; result.unconfirmed_balance = bal.m_mine_untrusted_pending; @@ -381,15 +383,15 @@ public: balances = getBalances(); return true; } - CAmount getBalance() override { return m_wallet->GetBalance().m_mine_trusted; } + CAmount getBalance() override { return GetBalance(*m_wallet).m_mine_trusted; } CAmount getAvailableBalance(const CCoinControl& coin_control) override { - return m_wallet->GetAvailableBalance(&coin_control); + return GetAvailableBalance(*m_wallet, &coin_control); } isminetype txinIsMine(const CTxIn& txin) override { LOCK(m_wallet->cs_wallet); - return m_wallet->IsMine(txin); + return InputIsMine(*m_wallet, txin); } isminetype txoutIsMine(const CTxOut& txout) override { @@ -404,13 +406,13 @@ public: CAmount getCredit(const CTxOut& txout, isminefilter filter) override { LOCK(m_wallet->cs_wallet); - return m_wallet->GetCredit(txout, filter); + return OutputGetCredit(*m_wallet, txout, filter); } CoinsList listCoins() override { LOCK(m_wallet->cs_wallet); CoinsList result; - for (const auto& entry : m_wallet->ListCoins()) { + for (const auto& entry : ListCoins(*m_wallet)) { auto& group = result[entry.first]; for (const auto& coin : entry.second) { group.emplace_back(COutPoint(coin.tx->GetHash(), coin.i), @@ -428,7 +430,7 @@ public: result.emplace_back(); auto it = m_wallet->mapWallet.find(output.hash); if (it != m_wallet->mapWallet.end()) { - int depth = it->second.GetDepthInMainChain(); + int depth = m_wallet->GetTxDepthInMainChain(it->second); if (depth >= 0) { result.back() = MakeWalletTxOut(*m_wallet, it->second, output.n, depth); } diff --git a/src/wallet/load.cpp b/src/wallet/load.cpp index 88e9b9c78f..a994976394 100644 --- a/src/wallet/load.cpp +++ b/src/wallet/load.cpp @@ -8,10 +8,12 @@ #include <fs.h> #include <interfaces/chain.h> #include <scheduler.h> +#include <util/check.h> #include <util/string.h> #include <util/system.h> #include <util/translation.h> #include <wallet/context.h> +#include <wallet/spend.h> #include <wallet/wallet.h> #include <wallet/walletdb.h> @@ -20,8 +22,10 @@ bool VerifyWallets(WalletContext& context) { interfaces::Chain& chain = *context.chain; - if (gArgs.IsArgSet("-walletdir")) { - fs::path wallet_dir = gArgs.GetArg("-walletdir", ""); + ArgsManager& args = *Assert(context.args); + + if (args.IsArgSet("-walletdir")) { + fs::path wallet_dir = args.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); @@ -36,7 +40,7 @@ bool VerifyWallets(WalletContext& context) chain.initError(strprintf(_("Specified -walletdir \"%s\" is a relative path"), wallet_dir.string())); return false; } - gArgs.ForceSetArg("-walletdir", canonical_wallet_dir.string()); + args.ForceSetArg("-walletdir", canonical_wallet_dir.string()); } LogPrintf("Using wallet directory %s\n", GetWalletDir().string()); @@ -45,7 +49,7 @@ bool VerifyWallets(WalletContext& context) // 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")) { + if (!args.IsArgSet("wallet")) { DatabaseOptions options; DatabaseStatus status; bilingual_str error_string; diff --git a/src/wallet/receive.cpp b/src/wallet/receive.cpp index de81dbf324..98706dcdf8 100644 --- a/src/wallet/receive.cpp +++ b/src/wallet/receive.cpp @@ -7,27 +7,27 @@ #include <wallet/transaction.h> #include <wallet/wallet.h> -isminetype CWallet::IsMine(const CTxIn &txin) const +isminetype InputIsMine(const CWallet& wallet, const CTxIn &txin) { - AssertLockHeld(cs_wallet); - std::map<uint256, CWalletTx>::const_iterator mi = mapWallet.find(txin.prevout.hash); - if (mi != mapWallet.end()) + AssertLockHeld(wallet.cs_wallet); + std::map<uint256, CWalletTx>::const_iterator mi = wallet.mapWallet.find(txin.prevout.hash); + if (mi != wallet.mapWallet.end()) { const CWalletTx& prev = (*mi).second; if (txin.prevout.n < prev.tx->vout.size()) - return IsMine(prev.tx->vout[txin.prevout.n]); + return wallet.IsMine(prev.tx->vout[txin.prevout.n]); } return ISMINE_NO; } -bool CWallet::IsAllFromMe(const CTransaction& tx, const isminefilter& filter) const +bool AllInputsMine(const CWallet& wallet, const CTransaction& tx, const isminefilter& filter) { - LOCK(cs_wallet); + LOCK(wallet.cs_wallet); for (const CTxIn& txin : tx.vin) { - auto mi = mapWallet.find(txin.prevout.hash); - if (mi == mapWallet.end()) + auto mi = wallet.mapWallet.find(txin.prevout.hash); + if (mi == wallet.mapWallet.end()) return false; // any unknown inputs can't be from us const CWalletTx& prev = (*mi).second; @@ -35,33 +35,33 @@ bool CWallet::IsAllFromMe(const CTransaction& tx, const isminefilter& filter) co if (txin.prevout.n >= prev.tx->vout.size()) return false; // invalid input! - if (!(IsMine(prev.tx->vout[txin.prevout.n]) & filter)) + if (!(wallet.IsMine(prev.tx->vout[txin.prevout.n]) & filter)) return false; } return true; } -CAmount CWallet::GetCredit(const CTxOut& txout, const isminefilter& filter) const +CAmount OutputGetCredit(const CWallet& wallet, const CTxOut& txout, const isminefilter& filter) { 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); + LOCK(wallet.cs_wallet); + return ((wallet.IsMine(txout) & filter) ? txout.nValue : 0); } -CAmount CWallet::GetCredit(const CTransaction& tx, const isminefilter& filter) const +CAmount TxGetCredit(const CWallet& wallet, const CTransaction& tx, const isminefilter& filter) { CAmount nCredit = 0; for (const CTxOut& txout : tx.vout) { - nCredit += GetCredit(txout, filter); + nCredit += OutputGetCredit(wallet, 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 +bool ScriptIsChange(const CWallet& wallet, const CScript& script) { // 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 @@ -70,179 +70,177 @@ bool CWallet::IsChange(const CScript& script) const // 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)) + AssertLockHeld(wallet.cs_wallet); + if (wallet.IsMine(script)) { CTxDestination address; if (!ExtractDestination(script, address)) return true; - if (!FindAddressBookEntry(address)) { + if (!wallet.FindAddressBookEntry(address)) { return true; } } return false; } -bool CWallet::IsChange(const CTxOut& txout) const +bool OutputIsChange(const CWallet& wallet, const CTxOut& txout) { - return IsChange(txout.scriptPubKey); + return ScriptIsChange(wallet, txout.scriptPubKey); } -CAmount CWallet::GetChange(const CTxOut& txout) const +CAmount OutputGetChange(const CWallet& wallet, const CTxOut& txout) { - AssertLockHeld(cs_wallet); + AssertLockHeld(wallet.cs_wallet); if (!MoneyRange(txout.nValue)) throw std::runtime_error(std::string(__func__) + ": value out of range"); - return (IsChange(txout) ? txout.nValue : 0); + return (OutputIsChange(wallet, txout) ? txout.nValue : 0); } -CAmount CWallet::GetChange(const CTransaction& tx) const +CAmount TxGetChange(const CWallet& wallet, const CTransaction& tx) { - LOCK(cs_wallet); + LOCK(wallet.cs_wallet); CAmount nChange = 0; for (const CTxOut& txout : tx.vout) { - nChange += GetChange(txout); + nChange += OutputGetChange(wallet, 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 +static CAmount GetCachableAmount(const CWallet& wallet, const CWalletTx& wtx, CWalletTx::AmountType type, const isminefilter& filter, bool recalculate = false) { - auto& amount = m_amounts[type]; + auto& amount = wtx.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; + amount.Set(filter, type == CWalletTx::DEBIT ? wallet.GetDebit(*wtx.tx, filter) : TxGetCredit(wallet, *wtx.tx, filter)); + wtx.m_is_cache_empty = false; } return amount.m_value[filter]; } -CAmount CWalletTx::GetCredit(const isminefilter& filter) const +CAmount CachedTxGetCredit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter) { // Must wait until coinbase is safely deep enough in the chain before valuing it - if (IsImmatureCoinBase()) + if (wallet.IsTxImmatureCoinBase(wtx)) return 0; CAmount credit = 0; if (filter & ISMINE_SPENDABLE) { // GetBalance can assume transactions in mapWallet won't change - credit += GetCachableAmount(CREDIT, ISMINE_SPENDABLE); + credit += GetCachableAmount(wallet, wtx, CWalletTx::CREDIT, ISMINE_SPENDABLE); } if (filter & ISMINE_WATCH_ONLY) { - credit += GetCachableAmount(CREDIT, ISMINE_WATCH_ONLY); + credit += GetCachableAmount(wallet, wtx, CWalletTx::CREDIT, ISMINE_WATCH_ONLY); } return credit; } -CAmount CWalletTx::GetDebit(const isminefilter& filter) const +CAmount CachedTxGetDebit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter) { - if (tx->vin.empty()) + if (wtx.tx->vin.empty()) return 0; CAmount debit = 0; if (filter & ISMINE_SPENDABLE) { - debit += GetCachableAmount(DEBIT, ISMINE_SPENDABLE); + debit += GetCachableAmount(wallet, wtx, CWalletTx::DEBIT, ISMINE_SPENDABLE); } if (filter & ISMINE_WATCH_ONLY) { - debit += GetCachableAmount(DEBIT, ISMINE_WATCH_ONLY); + debit += GetCachableAmount(wallet, wtx, CWalletTx::DEBIT, ISMINE_WATCH_ONLY); } return debit; } -CAmount CWalletTx::GetChange() const +CAmount CachedTxGetChange(const CWallet& wallet, const CWalletTx& wtx) { - if (fChangeCached) - return nChangeCached; - nChangeCached = pwallet->GetChange(*tx); - fChangeCached = true; - return nChangeCached; + if (wtx.fChangeCached) + return wtx.nChangeCached; + wtx.nChangeCached = TxGetChange(wallet, *wtx.tx); + wtx.fChangeCached = true; + return wtx.nChangeCached; } -CAmount CWalletTx::GetImmatureCredit(bool fUseCache) const +CAmount CachedTxGetImmatureCredit(const CWallet& wallet, const CWalletTx& wtx, bool fUseCache) { - if (IsImmatureCoinBase() && IsInMainChain()) { - return GetCachableAmount(IMMATURE_CREDIT, ISMINE_SPENDABLE, !fUseCache); + if (wallet.IsTxImmatureCoinBase(wtx) && wallet.IsTxInMainChain(wtx)) { + return GetCachableAmount(wallet, wtx, CWalletTx::IMMATURE_CREDIT, ISMINE_SPENDABLE, !fUseCache); } return 0; } -CAmount CWalletTx::GetImmatureWatchOnlyCredit(const bool fUseCache) const +CAmount CachedTxGetImmatureWatchOnlyCredit(const CWallet& wallet, const CWalletTx& wtx, const bool fUseCache) { - if (IsImmatureCoinBase() && IsInMainChain()) { - return GetCachableAmount(IMMATURE_CREDIT, ISMINE_WATCH_ONLY, !fUseCache); + if (wallet.IsTxImmatureCoinBase(wtx) && wallet.IsTxInMainChain(wtx)) { + return GetCachableAmount(wallet, wtx, CWalletTx::IMMATURE_CREDIT, ISMINE_WATCH_ONLY, !fUseCache); } return 0; } -CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter) const +CAmount CachedTxGetAvailableCredit(const CWallet& wallet, const CWalletTx& wtx, bool fUseCache, const isminefilter& filter) { - 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()) + if (wallet.IsTxImmatureCoinBase(wtx)) return 0; - if (fUseCache && allow_cache && m_amounts[AVAILABLE_CREDIT].m_cached[filter]) { - return m_amounts[AVAILABLE_CREDIT].m_value[filter]; + if (fUseCache && allow_cache && wtx.m_amounts[CWalletTx::AVAILABLE_CREDIT].m_cached[filter]) { + return wtx.m_amounts[CWalletTx::AVAILABLE_CREDIT].m_value[filter]; } - bool allow_used_addresses = (filter & ISMINE_USED) || !pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE); + bool allow_used_addresses = (filter & ISMINE_USED) || !wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE); CAmount nCredit = 0; - uint256 hashTx = GetHash(); - for (unsigned int i = 0; i < tx->vout.size(); i++) + uint256 hashTx = wtx.GetHash(); + for (unsigned int i = 0; i < wtx.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 (!wallet.IsSpent(hashTx, i) && (allow_used_addresses || !wallet.IsSpentKey(hashTx, i))) { + const CTxOut &txout = wtx.tx->vout[i]; + nCredit += OutputGetCredit(wallet, 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; + wtx.m_amounts[CWalletTx::AVAILABLE_CREDIT].Set(filter, nCredit); + wtx.m_is_cache_empty = false; } return nCredit; } -void CWalletTx::GetAmounts(std::list<COutputEntry>& listReceived, - std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter) const +void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx, + std::list<COutputEntry>& listReceived, + std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter) { nFee = 0; listReceived.clear(); listSent.clear(); // Compute fee: - CAmount nDebit = GetDebit(filter); + CAmount nDebit = CachedTxGetDebit(wallet, wtx, filter); if (nDebit > 0) // debit>0 means we signed/sent this transaction { - CAmount nValueOut = tx->GetValueOut(); + CAmount nValueOut = wtx.tx->GetValueOut(); nFee = nDebit - nValueOut; } - LOCK(pwallet->cs_wallet); + LOCK(wallet.cs_wallet); // Sent/received. - for (unsigned int i = 0; i < tx->vout.size(); ++i) + for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) { - const CTxOut& txout = tx->vout[i]; - isminetype fIsMine = pwallet->IsMine(txout); + const CTxOut& txout = wtx.tx->vout[i]; + isminetype fIsMine = wallet.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)) + if (OutputIsChange(wallet, txout)) continue; } else if (!(fIsMine & filter)) @@ -253,8 +251,8 @@ void CWalletTx::GetAmounts(std::list<COutputEntry>& listReceived, if (!ExtractDestination(txout.scriptPubKey, address) && !txout.scriptPubKey.IsUnspendable()) { - pwallet->WalletLogPrintf("CWalletTx::GetAmounts: Unknown transaction type found, txid %s\n", - this->GetHash().ToString()); + wallet.WalletLogPrintf("CWalletTx::GetAmounts: Unknown transaction type found, txid %s\n", + wtx.GetHash().ToString()); address = CNoDestination(); } @@ -271,16 +269,21 @@ void CWalletTx::GetAmounts(std::list<COutputEntry>& listReceived, } -bool CWallet::IsTrusted(const CWalletTx& wtx, std::set<uint256>& trusted_parents) const +bool CachedTxIsFromMe(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter) +{ + return (CachedTxGetDebit(wallet, wtx, filter) > 0); +} + +bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uint256>& trusted_parents) { - AssertLockHeld(cs_wallet); + AssertLockHeld(wallet.cs_wallet); // Quick answer in most cases - if (!chain().checkFinalTx(*wtx.tx)) return false; - int nDepth = wtx.GetDepthInMainChain(); + if (!wallet.chain().checkFinalTx(*wtx.tx)) return false; + int nDepth = wallet.GetTxDepthInMainChain(wtx); 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; + if (!wallet.m_spend_zero_conf_change || !CachedTxIsFromMe(wallet, wtx, ISMINE_ALL)) return false; // Don't trust unconfirmed transactions from us unless they are in the mempool. if (!wtx.InMempool()) return false; @@ -289,41 +292,41 @@ bool CWallet::IsTrusted(const CWalletTx& wtx, std::set<uint256>& trusted_parents for (const CTxIn& txin : wtx.tx->vin) { // Transactions not sent by us: not trusted - const CWalletTx* parent = GetWalletTx(txin.prevout.hash); + const CWalletTx* parent = wallet.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 (wallet.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; + if (!CachedTxIsTrusted(wallet, *parent, trusted_parents)) return false; trusted_parents.insert(parent->GetHash()); } return true; } -bool CWalletTx::IsTrusted() const +bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx) { std::set<uint256> trusted_parents; - LOCK(pwallet->cs_wallet); - return pwallet->IsTrusted(*this, trusted_parents); + LOCK(wallet.cs_wallet); + return CachedTxIsTrusted(wallet, wtx, trusted_parents); } -CWallet::Balance CWallet::GetBalance(const int min_depth, bool avoid_reuse) const +Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse) { Balance ret; isminefilter reuse_filter = avoid_reuse ? ISMINE_NO : ISMINE_USED; { - LOCK(cs_wallet); + LOCK(wallet.cs_wallet); std::set<uint256> trusted_parents; - for (const auto& entry : mapWallet) + for (const auto& entry : wallet.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)}; + const bool is_trusted{CachedTxIsTrusted(wallet, wtx, trusted_parents)}; + const int tx_depth{wallet.GetTxDepthInMainChain(wtx)}; + const CAmount tx_credit_mine{CachedTxGetAvailableCredit(wallet, wtx, /* fUseCache */ true, ISMINE_SPENDABLE | reuse_filter)}; + const CAmount tx_credit_watchonly{CachedTxGetAvailableCredit(wallet, wtx, /* 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; @@ -332,43 +335,43 @@ CWallet::Balance CWallet::GetBalance(const int min_depth, bool avoid_reuse) cons 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(); + ret.m_mine_immature += CachedTxGetImmatureCredit(wallet, wtx); + ret.m_watchonly_immature += CachedTxGetImmatureWatchOnlyCredit(wallet, wtx); } } return ret; } -std::map<CTxDestination, CAmount> CWallet::GetAddressBalances() const +std::map<CTxDestination, CAmount> GetAddressBalances(const CWallet& wallet) { std::map<CTxDestination, CAmount> balances; { - LOCK(cs_wallet); + LOCK(wallet.cs_wallet); std::set<uint256> trusted_parents; - for (const auto& walletEntry : mapWallet) + for (const auto& walletEntry : wallet.mapWallet) { const CWalletTx& wtx = walletEntry.second; - if (!IsTrusted(wtx, trusted_parents)) + if (!CachedTxIsTrusted(wallet, wtx, trusted_parents)) continue; - if (wtx.IsImmatureCoinBase()) + if (wallet.IsTxImmatureCoinBase(wtx)) continue; - int nDepth = wtx.GetDepthInMainChain(); - if (nDepth < (wtx.IsFromMe(ISMINE_ALL) ? 0 : 1)) + int nDepth = wallet.GetTxDepthInMainChain(wtx); + if (nDepth < (CachedTxIsFromMe(wallet, wtx, ISMINE_ALL) ? 0 : 1)) continue; for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) { CTxDestination addr; - if (!IsMine(wtx.tx->vout[i])) + if (!wallet.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; + CAmount n = wallet.IsSpent(walletEntry.first, i) ? 0 : wtx.tx->vout[i].nValue; balances[addr] += n; } } @@ -377,13 +380,13 @@ std::map<CTxDestination, CAmount> CWallet::GetAddressBalances() const return balances; } -std::set< std::set<CTxDestination> > CWallet::GetAddressGroupings() const +std::set< std::set<CTxDestination> > GetAddressGroupings(const CWallet& wallet) { - AssertLockHeld(cs_wallet); + AssertLockHeld(wallet.cs_wallet); std::set< std::set<CTxDestination> > groupings; std::set<CTxDestination> grouping; - for (const auto& walletEntry : mapWallet) + for (const auto& walletEntry : wallet.mapWallet) { const CWalletTx& wtx = walletEntry.second; @@ -394,9 +397,9 @@ std::set< std::set<CTxDestination> > CWallet::GetAddressGroupings() const for (const CTxIn& txin : wtx.tx->vin) { CTxDestination address; - if(!IsMine(txin)) /* If this input isn't mine, ignore it */ + if(!InputIsMine(wallet, txin)) /* If this input isn't mine, ignore it */ continue; - if(!ExtractDestination(mapWallet.at(txin.prevout.hash).tx->vout[txin.prevout.n].scriptPubKey, address)) + if(!ExtractDestination(wallet.mapWallet.at(txin.prevout.hash).tx->vout[txin.prevout.n].scriptPubKey, address)) continue; grouping.insert(address); any_mine = true; @@ -406,7 +409,7 @@ std::set< std::set<CTxDestination> > CWallet::GetAddressGroupings() const if (any_mine) { for (const CTxOut& txout : wtx.tx->vout) - if (IsChange(txout)) + if (OutputIsChange(wallet, txout)) { CTxDestination txoutAddr; if(!ExtractDestination(txout.scriptPubKey, txoutAddr)) @@ -423,7 +426,7 @@ std::set< std::set<CTxDestination> > CWallet::GetAddressGroupings() const // group lone addrs by themselves for (const auto& txout : wtx.tx->vout) - if (IsMine(txout)) + if (wallet.IsMine(txout)) { CTxDestination address; if(!ExtractDestination(txout.scriptPubKey, address)) diff --git a/src/wallet/receive.h b/src/wallet/receive.h index 8eead32413..b4b311636b 100644 --- a/src/wallet/receive.h +++ b/src/wallet/receive.h @@ -10,11 +10,55 @@ #include <wallet/transaction.h> #include <wallet/wallet.h> +isminetype InputIsMine(const CWallet& wallet, const CTxIn& txin) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); + +/** Returns whether all of the inputs match the filter */ +bool AllInputsMine(const CWallet& wallet, const CTransaction& tx, const isminefilter& filter); + +CAmount OutputGetCredit(const CWallet& wallet, const CTxOut& txout, const isminefilter& filter); +CAmount TxGetCredit(const CWallet& wallet, const CTransaction& tx, const isminefilter& filter); + +bool ScriptIsChange(const CWallet& wallet, const CScript& script) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); +bool OutputIsChange(const CWallet& wallet, const CTxOut& txout) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); +CAmount OutputGetChange(const CWallet& wallet, const CTxOut& txout) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); +CAmount TxGetChange(const CWallet& wallet, const CTransaction& tx); + +CAmount CachedTxGetCredit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter); +//! filter decides which addresses will count towards the debit +CAmount CachedTxGetDebit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter); +CAmount CachedTxGetChange(const CWallet& wallet, const CWalletTx& wtx); +CAmount CachedTxGetImmatureCredit(const CWallet& wallet, const CWalletTx& wtx, bool fUseCache = true); +CAmount CachedTxGetImmatureWatchOnlyCredit(const CWallet& wallet, const CWalletTx& wtx, const bool fUseCache = true); +// 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 CachedTxGetAvailableCredit(const CWallet& wallet, const CWalletTx& wtx, bool fUseCache = true, const isminefilter& filter = ISMINE_SPENDABLE) NO_THREAD_SAFETY_ANALYSIS; struct COutputEntry { CTxDestination destination; CAmount amount; int vout; }; +void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx, + std::list<COutputEntry>& listReceived, + std::list<COutputEntry>& listSent, + CAmount& nFee, const isminefilter& filter); +bool CachedTxIsFromMe(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter); +bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uint256>& trusted_parents) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); +bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx); + +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(const CWallet& wallet, int min_depth = 0, bool avoid_reuse = true); + +std::map<CTxDestination, CAmount> GetAddressBalances(const CWallet& wallet); +std::set<std::set<CTxDestination>> GetAddressGroupings(const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); #endif // BITCOIN_WALLET_RECEIVE_H diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 916f811f9b..ff9e10c5ad 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -31,7 +31,9 @@ #include <wallet/context.h> #include <wallet/feebumper.h> #include <wallet/load.h> +#include <wallet/receive.h> #include <wallet/rpcwallet.h> +#include <wallet/spend.h> #include <wallet/wallet.h> #include <wallet/walletdb.h> #include <wallet/walletutil.h> @@ -147,9 +149,10 @@ LegacyScriptPubKeyMan& EnsureLegacyScriptPubKeyMan(CWallet& wallet, bool also_cr return *spk_man; } -static void WalletTxToJSON(interfaces::Chain& chain, const CWalletTx& wtx, UniValue& entry) +static void WalletTxToJSON(const CWallet& wallet, const CWalletTx& wtx, UniValue& entry) { - int confirms = wtx.GetDepthInMainChain(); + interfaces::Chain& chain = wallet.chain(); + int confirms = wallet.GetTxDepthInMainChain(wtx); entry.pushKV("confirmations", confirms); if (wtx.IsCoinBase()) entry.pushKV("generated", true); @@ -162,12 +165,12 @@ static void WalletTxToJSON(interfaces::Chain& chain, const CWalletTx& wtx, UniVa CHECK_NONFATAL(chain.findBlock(wtx.m_confirm.hashBlock, FoundBlock().time(block_time))); entry.pushKV("blocktime", block_time); } else { - entry.pushKV("trusted", wtx.IsTrusted()); + entry.pushKV("trusted", CachedTxIsTrusted(wallet, wtx)); } uint256 hash = wtx.GetHash(); entry.pushKV("txid", hash.GetHex()); UniValue conflicts(UniValue::VARR); - for (const uint256& conflict : wtx.GetConflicts()) + for (const uint256& conflict : wallet.GetTxConflicts(wtx)) conflicts.push_back(conflict.GetHex()); entry.pushKV("walletconflicts", conflicts); entry.pushKV("time", wtx.GetTxTime()); @@ -423,7 +426,7 @@ UniValue SendMoney(CWallet& wallet, const CCoinControl &coin_control, std::vecto bilingual_str error; CTransactionRef tx; FeeCalculation fee_calc_out; - const bool fCreated = wallet.CreateTransaction(recipients, tx, nFeeRequired, nChangePosRet, error, coin_control, fee_calc_out, true); + const bool fCreated = CreateTransaction(wallet, recipients, tx, nFeeRequired, nChangePosRet, error, coin_control, fee_calc_out, true); if (!fCreated) { throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, error.original); } @@ -576,8 +579,8 @@ static RPCHelpMan listaddressgroupings() LOCK(pwallet->cs_wallet); UniValue jsonGroupings(UniValue::VARR); - std::map<CTxDestination, CAmount> balances = pwallet->GetAddressBalances(); - for (const std::set<CTxDestination>& grouping : pwallet->GetAddressGroupings()) { + std::map<CTxDestination, CAmount> balances = GetAddressBalances(*pwallet); + for (const std::set<CTxDestination>& grouping : GetAddressGroupings(*pwallet)) { UniValue jsonGrouping(UniValue::VARR); for (const CTxDestination& address : grouping) { @@ -686,7 +689,7 @@ static CAmount GetReceived(const CWallet& wallet, const UniValue& params, bool b CAmount amount = 0; for (const std::pair<const uint256, CWalletTx>& wtx_pair : wallet.mapWallet) { const CWalletTx& wtx = wtx_pair.second; - if (wtx.IsCoinBase() || !wallet.chain().checkFinalTx(*wtx.tx) || wtx.GetDepthInMainChain() < min_depth) { + if (wtx.IsCoinBase() || !wallet.chain().checkFinalTx(*wtx.tx) || wallet.GetTxDepthInMainChain(wtx) < min_depth) { continue; } @@ -826,7 +829,7 @@ static RPCHelpMan getbalance() bool avoid_reuse = GetAvoidReuseFlag(*pwallet, request.params[3]); - const auto bal = pwallet->GetBalance(min_depth, avoid_reuse); + const auto bal = GetBalance(*pwallet, min_depth, avoid_reuse); return ValueFromAmount(bal.m_mine_trusted + (include_watchonly ? bal.m_watchonly_trusted : 0)); }, @@ -851,7 +854,7 @@ static RPCHelpMan getunconfirmedbalance() LOCK(pwallet->cs_wallet); - return ValueFromAmount(pwallet->GetBalance().m_mine_untrusted_pending); + return ValueFromAmount(GetBalance(*pwallet).m_mine_untrusted_pending); }, }; } @@ -1085,7 +1088,7 @@ static UniValue ListReceived(const CWallet& wallet, const UniValue& params, bool continue; } - int nDepth = wtx.GetDepthInMainChain(); + int nDepth = wallet.GetTxDepthInMainChain(wtx); if (nDepth < nMinDepth) continue; @@ -1310,9 +1313,9 @@ static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nM std::list<COutputEntry> listReceived; std::list<COutputEntry> listSent; - wtx.GetAmounts(listReceived, listSent, nFee, filter_ismine); + CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine); - bool involvesWatchonly = wtx.IsFromMe(ISMINE_WATCH_ONLY); + bool involvesWatchonly = CachedTxIsFromMe(wallet, wtx, ISMINE_WATCH_ONLY); // Sent if (!filter_label) @@ -1333,14 +1336,14 @@ static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nM entry.pushKV("vout", s.vout); entry.pushKV("fee", ValueFromAmount(-nFee)); if (fLong) - WalletTxToJSON(wallet.chain(), wtx, entry); + WalletTxToJSON(wallet, wtx, entry); entry.pushKV("abandoned", wtx.isAbandoned()); ret.push_back(entry); } } // Received - if (listReceived.size() > 0 && wtx.GetDepthInMainChain() >= nMinDepth) { + if (listReceived.size() > 0 && wallet.GetTxDepthInMainChain(wtx) >= nMinDepth) { for (const COutputEntry& r : listReceived) { std::string label; @@ -1358,9 +1361,9 @@ static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nM MaybePushAddress(entry, r.destination); if (wtx.IsCoinBase()) { - if (wtx.GetDepthInMainChain() < 1) + if (wallet.GetTxDepthInMainChain(wtx) < 1) entry.pushKV("category", "orphan"); - else if (wtx.IsImmatureCoinBase()) + else if (wallet.IsTxImmatureCoinBase(wtx)) entry.pushKV("category", "immature"); else entry.pushKV("category", "generate"); @@ -1375,7 +1378,7 @@ static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nM } entry.pushKV("vout", r.vout); if (fLong) - WalletTxToJSON(wallet.chain(), wtx, entry); + WalletTxToJSON(wallet, wtx, entry); ret.push_back(entry); } } @@ -1615,7 +1618,7 @@ static RPCHelpMan listsinceblock() for (const std::pair<const uint256, CWalletTx>& pairWtx : wallet.mapWallet) { const CWalletTx& tx = pairWtx.second; - if (depth == -1 || abs(tx.GetDepthInMainChain()) < depth) { + if (depth == -1 || abs(wallet.GetTxDepthInMainChain(tx)) < depth) { ListTransactions(wallet, tx, 0, true, transactions, filter, nullptr /* filter_label */); } } @@ -1736,16 +1739,16 @@ static RPCHelpMan gettransaction() } const CWalletTx& wtx = it->second; - CAmount nCredit = wtx.GetCredit(filter); - CAmount nDebit = wtx.GetDebit(filter); + CAmount nCredit = CachedTxGetCredit(*pwallet, wtx, filter); + CAmount nDebit = CachedTxGetDebit(*pwallet, wtx, filter); CAmount nNet = nCredit - nDebit; - CAmount nFee = (wtx.IsFromMe(filter) ? wtx.tx->GetValueOut() - nDebit : 0); + CAmount nFee = (CachedTxIsFromMe(*pwallet, wtx, filter) ? wtx.tx->GetValueOut() - nDebit : 0); entry.pushKV("amount", ValueFromAmount(nNet - nFee)); - if (wtx.IsFromMe(filter)) + if (CachedTxIsFromMe(*pwallet, wtx, filter)) entry.pushKV("fee", ValueFromAmount(nFee)); - WalletTxToJSON(pwallet->chain(), wtx, entry); + WalletTxToJSON(*pwallet, wtx, entry); UniValue details(UniValue::VARR); ListTransactions(*pwallet, wtx, 0, false, details, filter, nullptr /* filter_label */); @@ -2384,7 +2387,7 @@ static RPCHelpMan getbalances() LOCK(wallet.cs_wallet); - const auto bal = wallet.GetBalance(); + const auto bal = GetBalance(wallet); UniValue balances{UniValue::VOBJ}; { UniValue balances_mine{UniValue::VOBJ}; @@ -2394,7 +2397,7 @@ static RPCHelpMan getbalances() if (wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)) { // If the AVOID_REUSE flag is set, bal has been set to just the un-reused address balance. Get // the total balance, and then subtract bal to get the reused address balance. - const auto full_bal = wallet.GetBalance(0, false); + const auto full_bal = GetBalance(wallet, 0, false); balances_mine.pushKV("used", ValueFromAmount(full_bal.m_mine_trusted + full_bal.m_mine_untrusted_pending - bal.m_mine_trusted - bal.m_mine_untrusted_pending)); } balances.pushKV("mine", balances_mine); @@ -2462,7 +2465,7 @@ static RPCHelpMan getwalletinfo() UniValue obj(UniValue::VOBJ); size_t kpExternalSize = pwallet->KeypoolCountExternalKeys(); - const auto bal = pwallet->GetBalance(); + const auto bal = GetBalance(*pwallet); int64_t kp_oldest = pwallet->GetOldestKeyPoolTime(); obj.pushKV("walletname", pwallet->GetName()); obj.pushKV("walletversion", pwallet->GetVersion()); @@ -3058,7 +3061,7 @@ static RPCHelpMan listunspent() cctl.m_max_depth = nMaxDepth; cctl.m_include_unsafe_inputs = include_unsafe; LOCK(pwallet->cs_wallet); - pwallet->AvailableCoins(vecOutputs, &cctl, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount); + AvailableCoins(*pwallet, vecOutputs, &cctl, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount); } LOCK(pwallet->cs_wallet); @@ -3274,7 +3277,7 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out, bilingual_str error; - if (!wallet.FundTransaction(tx, fee_out, change_position, error, lockUnspents, setSubtractFeeFromOutputs, coinControl)) { + if (!FundTransaction(wallet, tx, fee_out, change_position, error, lockUnspents, setSubtractFeeFromOutputs, coinControl)) { throw JSONRPCError(RPC_WALLET_ERROR, error.original); } } @@ -3959,7 +3962,7 @@ RPCHelpMan getaddressinfo() UniValue detail = DescribeWalletAddress(*pwallet, dest); ret.pushKVs(detail); - ret.pushKV("ischange", pwallet->IsChange(scriptPubKey)); + ret.pushKV("ischange", ScriptIsChange(*pwallet, scriptPubKey)); ScriptPubKeyMan* spk_man = pwallet->GetScriptPubKeyMan(scriptPubKey); if (spk_man) { diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index 3bb7134b24..4a7a268982 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -21,6 +21,11 @@ using interfaces::FoundBlock; static constexpr size_t OUTPUT_GROUP_MAX_ENTRIES{100}; +int GetTxSpendSize(const CWallet& wallet, const CWalletTx& wtx, unsigned int out, bool use_max_sig) +{ + return CalculateMaximumSignedInputSize(wtx.tx->vout[out], &wallet, use_max_sig); +} + std::string COutput::ToString() const { return strprintf("COutput(%s, %d, %d) [%s]", tx->GetHash().ToString(), i, nDepth, FormatMoney(tx->tx->vout[i].nValue)); @@ -64,33 +69,33 @@ TxSize CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *walle 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 +void AvailableCoins(const CWallet& wallet, std::vector<COutput>& vCoins, const CCoinControl* coinControl, const CAmount& nMinimumAmount, const CAmount& nMaximumAmount, const CAmount& nMinimumSumAmount, const uint64_t nMaximumCount) { - AssertLockHeld(cs_wallet); + AssertLockHeld(wallet.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); + bool allow_used_addresses = !wallet.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) + for (const auto& entry : wallet.mapWallet) { const uint256& wtxid = entry.first; const CWalletTx& wtx = entry.second; - if (!chain().checkFinalTx(*wtx.tx)) { + if (!wallet.chain().checkFinalTx(*wtx.tx)) { continue; } - if (wtx.IsImmatureCoinBase()) + if (wallet.IsTxImmatureCoinBase(wtx)) continue; - int nDepth = wtx.GetDepthInMainChain(); + int nDepth = wallet.GetTxDepthInMainChain(wtx); if (nDepth < 0) continue; @@ -99,7 +104,7 @@ void CWallet::AvailableCoins(std::vector<COutput>& vCoins, const CCoinControl* c if (nDepth == 0 && !wtx.InMempool()) continue; - bool safeTx = IsTrusted(wtx, trusted_parents); + bool safeTx = CachedTxIsTrusted(wallet, wtx, trusted_parents); // We should not consider coins from transactions that are replacing // other transactions. @@ -152,28 +157,28 @@ void CWallet::AvailableCoins(std::vector<COutput>& vCoins, const CCoinControl* c if (coinControl && coinControl->HasSelected() && !coinControl->fAllowOtherInputs && !coinControl->IsSelected(COutPoint(entry.first, i))) continue; - if (IsLockedCoin(entry.first, i)) + if (wallet.IsLockedCoin(entry.first, i)) continue; - if (IsSpent(wtxid, i)) + if (wallet.IsSpent(wtxid, i)) continue; - isminetype mine = IsMine(wtx.tx->vout[i]); + isminetype mine = wallet.IsMine(wtx.tx->vout[i]); if (mine == ISMINE_NO) { continue; } - if (!allow_used_addresses && IsSpentKey(wtxid, i)) { + if (!allow_used_addresses && wallet.IsSpentKey(wtxid, i)) { continue; } - std::unique_ptr<SigningProvider> provider = GetSolvingProvider(wtx.tx->vout[i].scriptPubKey); + std::unique_ptr<SigningProvider> provider = wallet.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))); + vCoins.push_back(COutput(wallet, wtx, i, nDepth, spendable, solvable, safeTx, (coinControl && coinControl->fAllowWatchOnly))); // Checks the sum amount of all UTXO's. if (nMinimumSumAmount != MAX_MONEY) { @@ -192,13 +197,13 @@ void CWallet::AvailableCoins(std::vector<COutput>& vCoins, const CCoinControl* c } } -CAmount CWallet::GetAvailableBalance(const CCoinControl* coinControl) const +CAmount GetAvailableBalance(const CWallet& wallet, const CCoinControl* coinControl) { - LOCK(cs_wallet); + LOCK(wallet.cs_wallet); CAmount balance = 0; std::vector<COutput> vCoins; - AvailableCoins(vCoins, coinControl); + AvailableCoins(wallet, vCoins, coinControl); for (const COutput& out : vCoins) { if (out.fSpendable) { balance += out.tx->tx->vout[out.i].nValue; @@ -207,16 +212,16 @@ CAmount CWallet::GetAvailableBalance(const CCoinControl* coinControl) const return balance; } -const CTxOut& CWallet::FindNonChangeParentOutput(const CTransaction& tx, int output) const +const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const CTransaction& tx, int output) { - AssertLockHeld(cs_wallet); + AssertLockHeld(wallet.cs_wallet); const CTransaction* ptx = &tx; int n = output; - while (IsChange(ptx->vout[n]) && ptx->vin.size() > 0) { + while (OutputIsChange(wallet, 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])) { + auto it = wallet.mapWallet.find(prevout.hash); + if (it == wallet.mapWallet.end() || it->second.tx->vout.size() <= prevout.n || + !wallet.IsMine(it->second.tx->vout[prevout.n])) { break; } ptx = it->second.tx.get(); @@ -225,39 +230,39 @@ const CTxOut& CWallet::FindNonChangeParentOutput(const CTransaction& tx, int out return ptx->vout[n]; } -std::map<CTxDestination, std::vector<COutput>> CWallet::ListCoins() const +std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet) { - AssertLockHeld(cs_wallet); + AssertLockHeld(wallet.cs_wallet); std::map<CTxDestination, std::vector<COutput>> result; std::vector<COutput> availableCoins; - AvailableCoins(availableCoins); + AvailableCoins(wallet, 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)) { + if ((coin.fSpendable || (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && coin.fSolvable)) && + ExtractDestination(FindNonChangeParentOutput(wallet, *coin.tx->tx, coin.i).scriptPubKey, address)) { result[address].emplace_back(std::move(coin)); } } std::vector<COutPoint> lockedCoins; - ListLockedCoins(lockedCoins); + wallet.ListLockedCoins(lockedCoins); // Include watch-only for LegacyScriptPubKeyMan wallets without private keys - const bool include_watch_only = GetLegacyScriptPubKeyMan() && IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); + const bool include_watch_only = wallet.GetLegacyScriptPubKeyMan() && wallet.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(); + auto it = wallet.mapWallet.find(output.hash); + if (it != wallet.mapWallet.end()) { + int depth = wallet.GetTxDepthInMainChain(it->second); if (depth >= 0 && output.n < it->second.tx->vout.size() && - IsMine(it->second.tx->vout[output.n]) == is_mine_filter + wallet.IsMine(it->second.tx->vout[output.n]) == is_mine_filter ) { CTxDestination address; - if (ExtractDestination(FindNonChangeParentOutput(*it->second.tx, output.n).scriptPubKey, address)) { + if (ExtractDestination(FindNonChangeParentOutput(wallet, *it->second.tx, output.n).scriptPubKey, address)) { result[address].emplace_back( - &it->second, output.n, depth, true /* spendable */, true /* solvable */, false /* safe */); + wallet, it->second, output.n, depth, true /* spendable */, true /* solvable */, false /* safe */); } } } @@ -266,7 +271,7 @@ std::map<CTxDestination, std::vector<COutput>> CWallet::ListCoins() const 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> GroupOutputs(const CWallet& wallet, const std::vector<COutput>& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only) { std::vector<OutputGroup> groups_out; @@ -277,12 +282,12 @@ std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutput>& outpu if (!output.fSpendable) continue; size_t ancestors, descendants; - chain().getTransactionAncestry(output.tx->GetHash(), ancestors, descendants); + wallet.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); + group.Insert(input_coin, output.nDepth, CachedTxIsFromMe(wallet, *output.tx, ISMINE_ALL), ancestors, descendants, positive_only); // Check the OutputGroup's eligibility. Only add the eligible ones. if (positive_only && group.GetSelectionAmount() <= 0) continue; @@ -303,7 +308,7 @@ std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutput>& outpu if (!output.fSpendable) continue; size_t ancestors, descendants; - chain().getTransactionAncestry(output.tx->GetHash(), ancestors, descendants); + wallet.chain().getTransactionAncestry(output.tx->GetHash(), ancestors, descendants); CInputCoin input_coin = output.GetInputCoin(); CScript spk = input_coin.txout.scriptPubKey; @@ -327,7 +332,7 @@ std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutput>& outpu } // Add the input_coin to group - group->Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants, positive_only); + group->Insert(input_coin, output.nDepth, CachedTxIsFromMe(wallet, *output.tx, ISMINE_ALL), ancestors, descendants, positive_only); } // Now we go through the entire map and pull out the OutputGroups @@ -352,25 +357,52 @@ std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutput>& outpu 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 +bool AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector<COutput> coins, + std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet, const CoinSelectionParams& coin_selection_params) { setCoinsRet.clear(); nValueRet = 0; + // Vector of results for use with waste calculation + // In order: calculated waste, selected inputs, selected input value (sum of input values) + // TODO: Use a struct representing the selection result + std::vector<std::tuple<CAmount, std::set<CInputCoin>, CAmount>> results; // 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; + std::vector<OutputGroup> positive_groups = GroupOutputs(wallet, coins, coin_selection_params, eligibility_filter, true /* positive_only */); + std::set<CInputCoin> bnb_coins; + CAmount bnb_value; + if (SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change, bnb_coins, bnb_value)) { + const auto waste = GetSelectionWaste(bnb_coins, /* cost of change */ CAmount(0), nTargetValue, !coin_selection_params.m_subtract_fee_outputs); + results.emplace_back(std::make_tuple(waste, std::move(bnb_coins), bnb_value)); } + // 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 */); + std::vector<OutputGroup> all_groups = GroupOutputs(wallet, 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); + std::set<CInputCoin> knapsack_coins; + CAmount knapsack_value; + if (KnapsackSolver(nTargetValue + coin_selection_params.m_change_fee, all_groups, knapsack_coins, knapsack_value)) { + const auto waste = GetSelectionWaste(knapsack_coins, coin_selection_params.m_cost_of_change, nTargetValue + coin_selection_params.m_change_fee, !coin_selection_params.m_subtract_fee_outputs); + results.emplace_back(std::make_tuple(waste, std::move(knapsack_coins), knapsack_value)); + } + + if (results.size() == 0) { + // No solution found + return false; + } + + // Choose the result with the least waste + // If the waste is the same, choose the one which spends more inputs. + const auto& best_result = std::min_element(results.begin(), results.end(), [](const auto& a, const auto& b) { + return std::get<0>(a) < std::get<0>(b) || (std::get<0>(a) == std::get<0>(b) && std::get<1>(a).size() > std::get<1>(b).size()); + }); + setCoinsRet = std::get<1>(*best_result); + nValueRet = std::get<2>(*best_result); + return true; } -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 +bool SelectCoins(const CWallet& wallet, const std::vector<COutput>& vAvailableCoins, const CAmount& nTargetValue, std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet, const CCoinControl& coin_control, CoinSelectionParams& coin_selection_params) { std::vector<COutput> vCoins(vAvailableCoins); CAmount value_to_select = nTargetValue; @@ -396,8 +428,8 @@ bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAm coin_control.ListSelected(vPresetInputs); for (const COutPoint& outpoint : vPresetInputs) { - std::map<uint256, CWalletTx>::const_iterator it = mapWallet.find(outpoint.hash); - if (it != mapWallet.end()) + std::map<uint256, CWalletTx>::const_iterator it = wallet.mapWallet.find(outpoint.hash); + if (it != wallet.mapWallet.end()) { const CWalletTx& wtx = it->second; // Clearly invalid input, fail @@ -405,7 +437,7 @@ bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAm return false; } // Just to calculate the marginal byte size - CInputCoin coin(wtx.tx, outpoint.n, wtx.GetSpendSize(outpoint.n, false)); + CInputCoin coin(wtx.tx, outpoint.n, GetTxSpendSize(wallet, wtx, outpoint.n, false)); nValueFromPresetInputs += coin.txout.nValue; if (coin.m_input_bytes <= 0) { return false; // Not solvable, can't estimate size for fee @@ -433,7 +465,7 @@ bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAm unsigned int limit_ancestor_count = 0; unsigned int limit_descendant_count = 0; - chain().getPackageLimits(limit_ancestor_count, limit_descendant_count); + wallet.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); @@ -456,32 +488,32 @@ bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAm // 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; + if (AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(1, 6, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params)) return true; + if (AttemptSelection(wallet, 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)), + if (wallet.m_spend_zero_conf_change) { + if (AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(0, 1, 2), vCoins, setCoinsRet, nValueRet, coin_selection_params)) return true; + if (AttemptSelection(wallet, 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), + if (AttemptSelection(wallet, 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 */), + if (AttemptSelection(wallet, 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, + && AttemptSelection(wallet, 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; @@ -489,7 +521,7 @@ bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAm // 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, + if (!fRejectLongChains && AttemptSelection(wallet, 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; @@ -568,7 +600,8 @@ static uint32_t GetLocktimeForNewTransaction(interfaces::Chain& chain, const uin return locktime; } -bool CWallet::CreateTransactionInternal( +static bool CreateTransactionInternal( + CWallet& wallet, const std::vector<CRecipient>& vecSend, CTransactionRef& tx, CAmount& nFeeRet, @@ -576,19 +609,22 @@ bool CWallet::CreateTransactionInternal( bilingual_str& error, const CCoinControl& coin_control, FeeCalculation& fee_calc_out, - bool sign) + bool sign) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) { - AssertLockHeld(cs_wallet); + AssertLockHeld(wallet.cs_wallet); CMutableTransaction txNew; // The resulting transaction that we make - txNew.nLockTime = GetLocktimeForNewTransaction(chain(), GetLastBlockHash(), GetLastBlockHeight()); + txNew.nLockTime = GetLocktimeForNewTransaction(wallet.chain(), wallet.GetLastBlockHash(), wallet.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; + // Set the long term feerate estimate to the wallet's consolidate feerate + coin_selection_params.m_long_term_feerate = wallet.m_consolidate_feerate; + 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); + const OutputType change_type = wallet.TransactionChangeType(coin_control.m_change_type ? *coin_control.m_change_type : wallet.m_default_change_type, vecSend); + ReserveDestination reservedest(&wallet, 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; @@ -632,7 +668,7 @@ bool CWallet::CreateTransactionInternal( 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); + int change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, &wallet); // 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) { @@ -642,28 +678,23 @@ bool CWallet::CreateTransactionInternal( } // Set discard feerate - coin_selection_params.m_discard_feerate = GetDiscardRate(*this); + coin_selection_params.m_discard_feerate = GetDiscardRate(wallet); // Get the fee rate to use effective values in coin selection FeeCalculation feeCalc; - coin_selection_params.m_effective_feerate = GetMinimumFeeRate(*this, coin_control, &feeCalc); + coin_selection_params.m_effective_feerate = GetMinimumFeeRate(wallet, 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) { + if (feeCalc.reason == FeeReason::FALLBACK && !wallet.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. @@ -685,7 +716,7 @@ bool CWallet::CreateTransactionInternal( coin_selection_params.tx_noinputs_size += ::GetSerializeSize(txout, PROTOCOL_VERSION); } - if (IsDust(txout, chain().relayDustFee())) + if (IsDust(txout, wallet.chain().relayDustFee())) { error = _("Transaction amount too small"); return false; @@ -699,12 +730,12 @@ bool CWallet::CreateTransactionInternal( // Get available coins std::vector<COutput> vAvailableCoins; - AvailableCoins(vAvailableCoins, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0); + AvailableCoins(wallet, 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)) + if (!SelectCoins(wallet, vAvailableCoins, /* nTargetValue */ selection_target, setCoins, inputs_sum, coin_control, coin_selection_params)) { error = _("Insufficient funds"); return false; @@ -742,13 +773,13 @@ bool CWallet::CreateTransactionInternal( // 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); + const uint32_t nSequence = coin_control.m_signal_bip125_rbf.value_or(wallet.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); + TxSize tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), &wallet, coin_control.fAllowWatchOnly); int nBytes = tx_sizes.vsize; if (nBytes < 0) { error = _("Signing transaction failed"); @@ -773,7 +804,7 @@ bool CWallet::CreateTransactionInternal( 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); + tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), &wallet, coin_control.fAllowWatchOnly); nBytes = tx_sizes.vsize; fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes); } @@ -810,7 +841,7 @@ bool CWallet::CreateTransactionInternal( } // Error if this output is reduced to be below dust - if (IsDust(txout, chain().relayDustFee())) { + if (IsDust(txout, wallet.chain().relayDustFee())) { if (txout.nValue < 0) { error = _("The transaction amount is too small to pay the fee"); } else { @@ -829,7 +860,7 @@ bool CWallet::CreateTransactionInternal( return false; } - if (sign && !SignTransaction(txNew)) { + if (sign && !wallet.SignTransaction(txNew)) { error = _("Signing transaction failed"); return false; } @@ -845,14 +876,14 @@ bool CWallet::CreateTransactionInternal( return false; } - if (nFeeRet > m_default_max_tx_fee) { + if (nFeeRet > wallet.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)) { + if (!wallet.chain().checkChainLimits(tx)) { error = _("Transaction has too long of a mempool chain"); return false; } @@ -863,7 +894,7 @@ bool CWallet::CreateTransactionInternal( 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", + wallet.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, @@ -874,7 +905,8 @@ bool CWallet::CreateTransactionInternal( return true; } -bool CWallet::CreateTransaction( +bool CreateTransaction( + CWallet& wallet, const std::vector<CRecipient>& vecSend, CTransactionRef& tx, CAmount& nFeeRet, @@ -894,23 +926,23 @@ bool CWallet::CreateTransaction( return false; } - LOCK(cs_wallet); + LOCK(wallet.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); + bool res = CreateTransactionInternal(wallet, 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) { + if (res && nFeeRet > 0 /* 0 means non-functional fee rate estimation */ && wallet.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 (CreateTransactionInternal(wallet, 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; - WalletLogPrintf("Fee non-grouped = %lld, grouped = %lld, using %s\n", nFeeRet, nFeeRet2, use_aps ? "grouped" : "non-grouped"); + const bool use_aps = nFeeRet2 <= nFeeRet + wallet.m_max_aps_fee; + wallet.WalletLogPrintf("Fee non-grouped = %lld, grouped = %lld, using %s\n", nFeeRet, nFeeRet2, use_aps ? "grouped" : "non-grouped"); if (use_aps) { tx = tx2; nFeeRet = nFeeRet2; @@ -921,7 +953,7 @@ bool CWallet::CreateTransaction( return res; } -bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, int& nChangePosInOut, bilingual_str& error, bool lockUnspents, const std::set<int>& setSubtractFeeFromOutputs, CCoinControl coinControl) +bool FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& nFeeRet, int& nChangePosInOut, bilingual_str& error, bool lockUnspents, const std::set<int>& setSubtractFeeFromOutputs, CCoinControl coinControl) { std::vector<CRecipient> vecSend; @@ -940,11 +972,11 @@ bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, int& nC // 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); + LOCK(wallet.cs_wallet); CTransactionRef tx_new; FeeCalculation fee_calc_out; - if (!CreateTransaction(vecSend, tx_new, nFeeRet, nChangePosInOut, error, coinControl, fee_calc_out, false)) { + if (!CreateTransaction(wallet, vecSend, tx_new, nFeeRet, nChangePosInOut, error, coinControl, fee_calc_out, false)) { return false; } @@ -965,7 +997,7 @@ bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, int& nC } if (lockUnspents) { - LockCoin(txin.prevout); + wallet.LockCoin(txin.prevout); } } diff --git a/src/wallet/spend.h b/src/wallet/spend.h index 03f9a7c2b5..e39f134dc3 100644 --- a/src/wallet/spend.h +++ b/src/wallet/spend.h @@ -9,6 +9,9 @@ #include <wallet/transaction.h> #include <wallet/wallet.h> +/** Get the marginal bytes if spending the specified output from this transaction */ +int GetTxSpendSize(const CWallet& wallet, const CWalletTx& wtx, unsigned int out, bool use_max_sig = false); + class COutput { public: @@ -43,13 +46,13 @@ public: */ bool fSafe; - COutput(const CWalletTx *txIn, int iIn, int nDepthIn, bool fSpendableIn, bool fSolvableIn, bool fSafeIn, bool use_max_sig_in = false) + COutput(const CWallet& wallet, const CWalletTx& wtx, 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; + tx = &wtx; 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); + if (fSpendable) { + nInputBytes = GetTxSpendSize(wallet, wtx, i, use_max_sig); } } @@ -61,4 +64,76 @@ public: } }; +//Get the marginal bytes of spending the specified output +int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* pwallet, bool use_max_sig = 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 AllInputsMine). */ +TxSize CalculateMaximumSignedTxSize(const CTransaction& tx, const CWallet* wallet, const std::vector<CTxOut>& txouts, bool use_max_sig = false); +TxSize CalculateMaximumSignedTxSize(const CTransaction& tx, const CWallet* wallet, bool use_max_sig = false) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet); + +/** + * populate vCoins with vector of available COutputs. + */ +void AvailableCoins(const CWallet& wallet, 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) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); + +CAmount GetAvailableBalance(const CWallet& wallet, const CCoinControl* coinControl = nullptr); + +/** + * Find non-change parent output. + */ +const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const CTransaction& tx, int output) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); + +/** + * Return list of available coins and locked coins grouped by non-change output address. + */ +std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); + +std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<COutput>& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only); + +/** + * 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 CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector<COutput> coins, + std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet, const CoinSelectionParams& coin_selection_params); + +/** + * 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 CWallet& wallet, const std::vector<COutput>& vAvailableCoins, const CAmount& nTargetValue, std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet, + const CCoinControl& coin_control, CoinSelectionParams& coin_selection_params) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); + +/** + * 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(CWallet& wallet, 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); + +/** + * Insert additional inputs into the transaction by + * calling CreateTransaction(); + */ +bool FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& nFeeRet, int& nChangePosInOut, bilingual_str& error, bool lockUnspents, const std::set<int>& setSubtractFeeFromOutputs, CCoinControl); + #endif // BITCOIN_WALLET_SPEND_H diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp index 3488ae3526..f901679efc 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -10,6 +10,7 @@ #include <util/translation.h> #include <wallet/coincontrol.h> #include <wallet/coinselection.h> +#include <wallet/spend.h> #include <wallet/test/wallet_test_fixture.h> #include <wallet/wallet.h> @@ -49,12 +50,16 @@ static void add_coin(const CAmount& nValue, int nInput, std::vector<CInputCoin>& set.emplace_back(MakeTransactionRef(tx), nInput); } -static void add_coin(const CAmount& nValue, int nInput, CoinSet& set) +static void add_coin(const CAmount& nValue, int nInput, CoinSet& set, CAmount fee = 0, CAmount long_term_fee = 0) { CMutableTransaction tx; tx.vout.resize(nInput + 1); tx.vout[nInput].nValue = nValue; - set.emplace(MakeTransactionRef(tx), nInput); + CInputCoin coin(MakeTransactionRef(tx), nInput); + coin.effective_value = nValue - fee; + coin.m_fee = fee; + coin.m_long_term_fee = long_term_fee; + set.insert(coin); } static void add_coin(CWallet& wallet, const CAmount& nValue, int nAge = 6*24, bool fIsFromMe = false, int nInput=0, bool spendable = false) @@ -83,7 +88,7 @@ static void add_coin(CWallet& wallet, const CAmount& nValue, int nAge = 6*24, bo 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 */); + COutput output(wallet, *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) @@ -137,6 +142,13 @@ inline std::vector<OutputGroup>& GroupCoins(const std::vector<COutput>& coins) return static_groups; } +inline std::vector<OutputGroup>& KnapsackGroupOutputs(const CoinEligibilityFilter& filter) +{ + static std::vector<OutputGroup> static_groups; + static_groups = GroupOutputs(testWallet, vCoins, coin_selection_params, filter, /* positive_only */false); + return static_groups; +} + // Branch and bound coin selection tests BOOST_AUTO_TEST_CASE(bnb_search_test) { @@ -281,14 +293,14 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) 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)); + BOOST_CHECK(!SelectCoinsBnB(GroupCoins(vCoins), 1 * CENT, coin_selection_params_bnb.m_cost_of_change, setCoinsRet, nValueRet)); // 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(SelectCoinsBnB(GroupCoins(vCoins), 1 * CENT, coin_selection_params_bnb.m_cost_of_change, setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 1 * CENT); // Make sure that can use BnB when there are preset inputs @@ -305,7 +317,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) 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_CHECK(SelectCoins(*wallet, vCoins, 10 * CENT, setCoinsRet, nValueRet, coin_control, coin_selection_params_bnb)); } } @@ -323,24 +335,24 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) 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)); + BOOST_CHECK(!KnapsackSolver(1 * CENT, KnapsackGroupOutputs(filter_standard), setCoinsRet, nValueRet)); 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)); + BOOST_CHECK(!KnapsackSolver(1 * CENT, KnapsackGroupOutputs(filter_standard), setCoinsRet, nValueRet)); // but we can find a new 1 cent - BOOST_CHECK( testWallet.AttemptSelection( 1 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(1 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); 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)); + BOOST_CHECK(!KnapsackSolver(3 * CENT, KnapsackGroupOutputs(filter_standard), setCoinsRet, nValueRet)); // we can make 3 cents of new coins - BOOST_CHECK( testWallet.AttemptSelection( 3 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(3 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 3 * CENT); add_coin(5*CENT); // add a mature 5 cent coin, @@ -350,33 +362,33 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) // 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)); + BOOST_CHECK(!KnapsackSolver(38 * CENT, KnapsackGroupOutputs(filter_standard), setCoinsRet, nValueRet)); // 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)); + BOOST_CHECK(!KnapsackSolver(38 * CENT, KnapsackGroupOutputs(filter_standard_extra), setCoinsRet, nValueRet)); // 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(KnapsackSolver(37 * CENT, KnapsackGroupOutputs(filter_standard), setCoinsRet, nValueRet)); 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(KnapsackSolver(38 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); 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(KnapsackSolver(34 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); 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(KnapsackSolver(7 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); 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(KnapsackSolver(8 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); 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(KnapsackSolver(9 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 10 * CENT); BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); @@ -390,30 +402,30 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) 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)); + BOOST_CHECK(KnapsackSolver(71 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); + BOOST_CHECK(!KnapsackSolver(72 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); // 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(KnapsackSolver(16 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); 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(KnapsackSolver(16 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); 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(KnapsackSolver(16 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); 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(KnapsackSolver(11 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 11 * CENT); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); @@ -422,11 +434,11 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) 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(KnapsackSolver(95 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 1 * COIN); // we should get 1 BTC 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(KnapsackSolver(195 * CENT, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 2 * COIN); // we should get 2 BTC in 1 coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); @@ -441,14 +453,14 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) // 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(KnapsackSolver(MIN_CHANGE, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); 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(KnapsackSolver(1 * MIN_CHANGE, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 1 * MIN_CHANGE); // we should get the exact amount // if we add more small coins: @@ -456,7 +468,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) 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(KnapsackSolver(1 * MIN_CHANGE, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 1 * MIN_CHANGE); // we should get the exact amount // run the 'mtgox' test (see https://blockexplorer.com/tx/29a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf) @@ -465,7 +477,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) 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(KnapsackSolver(500000 * COIN, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 500000 * COIN); // we should get the exact amount BOOST_CHECK_EQUAL(setCoinsRet.size(), 10U); // in ten coins @@ -478,7 +490,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) 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(KnapsackSolver(1 * MIN_CHANGE, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 1111 * MIN_CHANGE); // we get the bigger coin BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); @@ -488,7 +500,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) 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(KnapsackSolver(MIN_CHANGE, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); 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 @@ -499,12 +511,12 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) 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(KnapsackSolver(MIN_CHANGE * 10001 / 100, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); 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(KnapsackSolver(MIN_CHANGE * 9990 / 100, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 101 * MIN_CHANGE); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); } @@ -518,7 +530,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) // 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)); + BOOST_CHECK(KnapsackSolver(2000, KnapsackGroupOutputs(filter_confirmed), setCoinsRet, nValueRet)); if (amt - 2000 < MIN_CHANGE) { // needs more than one input: @@ -603,7 +615,7 @@ BOOST_AUTO_TEST_CASE(ApproximateBestSubset) add_coin(1000 * COIN); add_coin(3 * COIN); - BOOST_CHECK(testWallet.AttemptSelection(1003 * COIN, filter_standard, vCoins, setCoinsRet, nValueRet, coin_selection_params)); + BOOST_CHECK(KnapsackSolver(1003 * COIN, KnapsackGroupOutputs(filter_standard), setCoinsRet, nValueRet)); BOOST_CHECK_EQUAL(nValueRet, 1003 * COIN); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); @@ -646,9 +658,78 @@ BOOST_AUTO_TEST_CASE(SelectCoins_test) CoinSet out_set; CAmount out_value = 0; CCoinControl cc; - BOOST_CHECK(testWallet.SelectCoins(vCoins, target, out_set, out_value, cc, cs_params)); + BOOST_CHECK(SelectCoins(testWallet, vCoins, target, out_set, out_value, cc, cs_params)); BOOST_CHECK_GE(out_value, target); } } +BOOST_AUTO_TEST_CASE(waste_test) +{ + CoinSet selection; + const CAmount fee{100}; + const CAmount change_cost{125}; + const CAmount fee_diff{40}; + const CAmount in_amt{3 * COIN}; + const CAmount target{2 * COIN}; + const CAmount excess{in_amt - fee * 2 - target}; + + // Waste with change is the change cost and difference between fee and long term fee + add_coin(1 * COIN, 1, selection, fee, fee - fee_diff); + add_coin(2 * COIN, 2, selection, fee, fee - fee_diff); + const CAmount waste1 = GetSelectionWaste(selection, change_cost, target); + BOOST_CHECK_EQUAL(fee_diff * 2 + change_cost, waste1); + selection.clear(); + + // Waste without change is the excess and difference between fee and long term fee + add_coin(1 * COIN, 1, selection, fee, fee - fee_diff); + add_coin(2 * COIN, 2, selection, fee, fee - fee_diff); + const CAmount waste_nochange1 = GetSelectionWaste(selection, 0, target); + BOOST_CHECK_EQUAL(fee_diff * 2 + excess, waste_nochange1); + selection.clear(); + + // Waste with change and fee == long term fee is just cost of change + add_coin(1 * COIN, 1, selection, fee, fee); + add_coin(2 * COIN, 2, selection, fee, fee); + BOOST_CHECK_EQUAL(change_cost, GetSelectionWaste(selection, change_cost, target)); + selection.clear(); + + // Waste without change and fee == long term fee is just the excess + add_coin(1 * COIN, 1, selection, fee, fee); + add_coin(2 * COIN, 2, selection, fee, fee); + BOOST_CHECK_EQUAL(excess, GetSelectionWaste(selection, 0, target)); + selection.clear(); + + // Waste will be greater when fee is greater, but long term fee is the same + add_coin(1 * COIN, 1, selection, fee * 2, fee - fee_diff); + add_coin(2 * COIN, 2, selection, fee * 2, fee - fee_diff); + const CAmount waste2 = GetSelectionWaste(selection, change_cost, target); + BOOST_CHECK_GT(waste2, waste1); + selection.clear(); + + // Waste with change is the change cost and difference between fee and long term fee + // With long term fee greater than fee, waste should be less than when long term fee is less than fee + add_coin(1 * COIN, 1, selection, fee, fee + fee_diff); + add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); + const CAmount waste3 = GetSelectionWaste(selection, change_cost, target); + BOOST_CHECK_EQUAL(fee_diff * -2 + change_cost, waste3); + BOOST_CHECK_LT(waste3, waste1); + selection.clear(); + + // Waste without change is the excess and difference between fee and long term fee + // With long term fee greater than fee, waste should be less than when long term fee is less than fee + add_coin(1 * COIN, 1, selection, fee, fee + fee_diff); + add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); + const CAmount waste_nochange2 = GetSelectionWaste(selection, 0, target); + BOOST_CHECK_EQUAL(fee_diff * -2 + excess, waste_nochange2); + BOOST_CHECK_LT(waste_nochange2, waste_nochange1); + selection.clear(); + + // 0 Waste only when fee == long term fee, no change, and no excess + add_coin(1 * COIN, 1, selection, fee, fee); + add_coin(2 * COIN, 2, selection, fee, fee); + const CAmount exact_target = in_amt - 2 * fee; + BOOST_CHECK_EQUAL(0, GetSelectionWaste(selection, 0, exact_target)); + +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/test/db_tests.cpp b/src/wallet/test/db_tests.cpp index 17f5264b45..16cb7e0baf 100644 --- a/src/wallet/test/db_tests.cpp +++ b/src/wallet/test/db_tests.cpp @@ -25,7 +25,11 @@ 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; diff --git a/src/wallet/test/init_test_fixture.cpp b/src/wallet/test/init_test_fixture.cpp index dd9354848d..53c972c46d 100644 --- a/src/wallet/test/init_test_fixture.cpp +++ b/src/wallet/test/init_test_fixture.cpp @@ -32,7 +32,11 @@ InitWalletDirTestingSetup::InitWalletDirTestingSetup(const std::string& chainNam 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(); } diff --git a/src/wallet/test/psbt_wallet_tests.cpp b/src/wallet/test/psbt_wallet_tests.cpp index 1cefa386b7..8a97f7779d 100644 --- a/src/wallet/test/psbt_wallet_tests.cpp +++ b/src/wallet/test/psbt_wallet_tests.cpp @@ -22,12 +22,12 @@ BOOST_AUTO_TEST_CASE(psbt_updater_test) 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)); + m_wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(prev_tx1->GetHash()), std::forward_as_tuple(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)); + m_wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(prev_tx2->GetHash()), std::forward_as_tuple(prev_tx2)); // Add scripts CScript rs1; diff --git a/src/wallet/test/spend_tests.cpp b/src/wallet/test/spend_tests.cpp index 8821f680b3..e779b2450f 100644 --- a/src/wallet/test/spend_tests.cpp +++ b/src/wallet/test/spend_tests.cpp @@ -5,6 +5,7 @@ #include <policy/fees.h> #include <validation.h> #include <wallet/coincontrol.h> +#include <wallet/spend.h> #include <wallet/test/util.h> #include <wallet/test/wallet_test_fixture.h> @@ -32,7 +33,7 @@ BOOST_FIXTURE_TEST_CASE(SubtractFee, TestChain100Setup) coin_control.m_feerate.emplace(10000); coin_control.fOverrideFeeRate = true; FeeCalculation fee_calc; - BOOST_CHECK(wallet->CreateTransaction({recipient}, tx, fee, change_pos, error, coin_control, fee_calc)); + BOOST_CHECK(CreateTransaction(*wallet, {recipient}, tx, fee, change_pos, error, coin_control, fee_calc)); BOOST_CHECK_EQUAL(tx->vout.size(), 1); BOOST_CHECK_EQUAL(tx->vout[0].nValue, recipient.nAmount + leftover_input_amount - fee); BOOST_CHECK_GT(fee, 0); diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 9fcea5826b..12a22f458a 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -21,6 +21,8 @@ #include <validation.h> #include <wallet/coincontrol.h> #include <wallet/context.h> +#include <wallet/receive.h> +#include <wallet/spend.h> #include <wallet/test/util.h> #include <wallet/test/wallet_test_fixture.h> @@ -103,7 +105,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) 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); + BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0); } // Verify ScanForWalletTransactions picks up transactions in both the old @@ -122,7 +124,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) 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, 100 * COIN); + BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 100 * COIN); } // Prune the older block file. @@ -148,7 +150,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) 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, 50 * COIN); + BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 50 * COIN); } // Prune the remaining block file. @@ -173,7 +175,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) 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_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0); } } @@ -200,6 +202,7 @@ BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup) wallet->SetupLegacyScriptPubKeyMan(); WITH_LOCK(wallet->cs_wallet, wallet->SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash())); WalletContext context; + context.args = &gArgs; AddWallet(context, wallet); UniValue keys; keys.setArray(); @@ -260,6 +263,7 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) // Import key into wallet and call dumpwallet to create backup file. { WalletContext context; + context.args = &gArgs; std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateDummyWalletDatabase()); { auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan(); @@ -287,6 +291,7 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) wallet->SetupLegacyScriptPubKeyMan(); WalletContext context; + context.args = &gArgs; JSONRPCRequest request; request.context = &context; request.params.setArray(); @@ -316,7 +321,7 @@ 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()); + CWalletTx wtx(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()); @@ -326,13 +331,13 @@ BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup) // 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); + BOOST_CHECK_EQUAL(CachedTxGetImmatureCredit(wallet, wtx), 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(), 50*COIN); + BOOST_CHECK_EQUAL(CachedTxGetImmatureCredit(wallet, wtx), 50*COIN); } static int64_t AddTx(ChainstateManager& chainman, CWallet& wallet, uint32_t lockTime, int64_t mockTime, int64_t blockTime) @@ -503,7 +508,7 @@ public: CCoinControl dummy; FeeCalculation fee_calc_out; { - BOOST_CHECK(wallet->CreateTransaction({recipient}, tx, fee, changePos, error, dummy, fee_calc_out)); + BOOST_CHECK(CreateTransaction(*wallet, {recipient}, tx, fee, changePos, error, dummy, fee_calc_out)); } wallet->CommitTransaction(tx, {}, {}); CMutableTransaction blocktx; @@ -525,7 +530,7 @@ public: std::unique_ptr<CWallet> wallet; }; -BOOST_FIXTURE_TEST_CASE(ListCoins, ListCoinsTestingSetup) +BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup) { std::string coinbaseAddress = coinbaseKey.GetPubKey().GetID().ToString(); @@ -534,14 +539,14 @@ BOOST_FIXTURE_TEST_CASE(ListCoins, ListCoinsTestingSetup) std::map<CTxDestination, std::vector<COutput>> list; { LOCK(wallet->cs_wallet); - list = wallet->ListCoins(); + list = ListCoins(*wallet); } 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(50 * COIN, wallet->GetAvailableBalance()); + BOOST_CHECK_EQUAL(50 * COIN, GetAvailableBalance(*wallet)); // Add a transaction creating a change address, and confirm ListCoins still // returns the coin associated with the change address underneath the @@ -550,7 +555,7 @@ BOOST_FIXTURE_TEST_CASE(ListCoins, ListCoinsTestingSetup) AddTx(CRecipient{GetScriptForRawPubKey({}), 1 * COIN, false /* subtract fee */}); { LOCK(wallet->cs_wallet); - list = wallet->ListCoins(); + list = ListCoins(*wallet); } BOOST_CHECK_EQUAL(list.size(), 1U); BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress); @@ -560,7 +565,7 @@ BOOST_FIXTURE_TEST_CASE(ListCoins, ListCoinsTestingSetup) { LOCK(wallet->cs_wallet); std::vector<COutput> available; - wallet->AvailableCoins(available); + AvailableCoins(*wallet, available); BOOST_CHECK_EQUAL(available.size(), 2U); } for (const auto& group : list) { @@ -572,14 +577,14 @@ BOOST_FIXTURE_TEST_CASE(ListCoins, ListCoinsTestingSetup) { LOCK(wallet->cs_wallet); std::vector<COutput> available; - wallet->AvailableCoins(available); + AvailableCoins(*wallet, 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(); + list = ListCoins(*wallet); } BOOST_CHECK_EQUAL(list.size(), 1U); BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress); @@ -685,6 +690,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) gArgs.ForceSetArg("-unsafesqlitesync", "1"); // Create new wallet with known key and unload it. WalletContext context; + context.args = &gArgs; context.chain = m_node.chain.get(); auto wallet = TestLoadWallet(context); CKey key; @@ -781,6 +787,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) { WalletContext context; + context.args = &gArgs; auto wallet = TestLoadWallet(context); BOOST_CHECK(wallet); UnloadWallet(std::move(wallet)); @@ -790,6 +797,7 @@ BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup) { gArgs.ForceSetArg("-unsafesqlitesync", "1"); WalletContext context; + context.args = &gArgs; context.chain = m_node.chain.get(); auto wallet = TestLoadWallet(context); CKey key; diff --git a/src/wallet/transaction.h b/src/wallet/transaction.h index 131faefe0b..094221adf2 100644 --- a/src/wallet/transaction.h +++ b/src/wallet/transaction.h @@ -17,12 +17,8 @@ #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) { @@ -34,6 +30,7 @@ static inline void ReadOrderPos(int64_t& nOrderPos, mapValue_t& mapValue) nOrderPos = atoi64(mapValue["n"]); } + static inline void WriteOrderPos(const int64_t& nOrderPos, mapValue_t& mapValue) { if (nOrderPos == -1) @@ -68,8 +65,6 @@ public: 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. */ @@ -126,7 +121,6 @@ public: // 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 @@ -139,9 +133,8 @@ public: mutable bool fInMempool; mutable CAmount nChangeCached; - CWalletTx(const CWallet* wallet, CTransactionRef arg) - : pwallet(wallet), - tx(std::move(arg)) + CWalletTx(CTransactionRef arg) + : tx(std::move(arg)) { Init(); } @@ -264,72 +257,13 @@ public: 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() { @@ -346,7 +280,6 @@ public: 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 diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index dbcfc4dac3..f3af9885a2 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -581,7 +581,7 @@ bool CWallet::IsSpent(const uint256& hash, unsigned int n) const const uint256& wtxid = it->second; std::map<uint256, CWalletTx>::const_iterator mit = mapWallet.find(wtxid); if (mit != mapWallet.end()) { - int depth = mit->second.GetDepthInMainChain(); + int depth = GetTxDepthInMainChain(mit->second); if (depth > 0 || (depth == 0 && !mit->second.isAbandoned())) return true; // Spent } @@ -900,7 +900,7 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const CWalletTx::Confirmatio } // Inserts only if not already there, returns tx inserted or tx found - auto ret = mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(hash), std::forward_as_tuple(this, tx)); + auto ret = mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(hash), std::forward_as_tuple(tx)); CWalletTx& wtx = (*ret.first).second; bool fInsertedNew = ret.second; bool fUpdated = update_wtx && update_wtx(wtx, fInsertedNew); @@ -984,7 +984,7 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const CWalletTx::Confirmatio bool CWallet::LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx) { - const auto& ins = mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(hash), std::forward_as_tuple(this, nullptr)); + const auto& ins = mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(hash), std::forward_as_tuple(nullptr)); CWalletTx& wtx = ins.first->second; if (!fill_wtx(wtx, ins.second)) { return false; @@ -1074,7 +1074,7 @@ bool CWallet::TransactionCanBeAbandoned(const uint256& hashTx) const { LOCK(cs_wallet); const CWalletTx* wtx = GetWalletTx(hashTx); - return wtx && !wtx->isAbandoned() && wtx->GetDepthInMainChain() == 0 && !wtx->InMempool(); + return wtx && !wtx->isAbandoned() && GetTxDepthInMainChain(*wtx) == 0 && !wtx->InMempool(); } void CWallet::MarkInputsDirty(const CTransactionRef& tx) @@ -1100,7 +1100,7 @@ bool CWallet::AbandonTransaction(const uint256& hashTx) auto it = mapWallet.find(hashTx); assert(it != mapWallet.end()); const CWalletTx& origtx = it->second; - if (origtx.GetDepthInMainChain() != 0 || origtx.InMempool()) { + if (GetTxDepthInMainChain(origtx) != 0 || origtx.InMempool()) { return false; } @@ -1113,7 +1113,7 @@ bool CWallet::AbandonTransaction(const uint256& hashTx) auto it = mapWallet.find(now); assert(it != mapWallet.end()); CWalletTx& wtx = it->second; - int currentconfirm = wtx.GetDepthInMainChain(); + int currentconfirm = GetTxDepthInMainChain(wtx); // If the orig tx was not in block, none of its spends can be assert(currentconfirm <= 0); // if (currentconfirm < 0) {Tx and spends are already conflicted, no need to abandon} @@ -1168,7 +1168,7 @@ void CWallet::MarkConflicted(const uint256& hashBlock, int conflicting_height, c auto it = mapWallet.find(now); assert(it != mapWallet.end()); CWalletTx& wtx = it->second; - int currentconfirm = wtx.GetDepthInMainChain(); + int currentconfirm = GetTxDepthInMainChain(wtx); if (conflictconfirms < currentconfirm) { // Block is 'more conflicted' than current confirm; update. // Mark transaction as conflicted with this block. @@ -1364,9 +1364,10 @@ CAmount CWallet::GetDebit(const CTransaction& tx, const isminefilter& filter) co bool CWallet::IsHDEnabled() const { // All Active ScriptPubKeyMans must be HD for this to be true - bool result = true; + bool result = false; for (const auto& spk_man : GetActiveScriptPubKeyMans()) { - result &= spk_man->IsHDEnabled(); + if (!spk_man->IsHDEnabled()) return false; + result = true; } return result; } @@ -1697,7 +1698,7 @@ void CWallet::ReacceptWalletTransactions() CWalletTx& wtx = item.second; assert(wtx.GetHash() == wtxid); - int nDepth = wtx.GetDepthInMainChain(); + int nDepth = GetTxDepthInMainChain(wtx); if (!wtx.IsCoinBase() && (nDepth == 0 && !wtx.isAbandoned())) { mapSorted.insert(std::make_pair(wtx.nOrderPos, &wtx)); @@ -1708,24 +1709,24 @@ void CWallet::ReacceptWalletTransactions() for (const std::pair<const int64_t, CWalletTx*>& item : mapSorted) { CWalletTx& wtx = *(item.second); std::string unused_err_string; - wtx.SubmitMemoryPoolAndRelay(unused_err_string, false); + SubmitTxMemoryPoolAndRelay(wtx, unused_err_string, false); } } -bool CWalletTx::SubmitMemoryPoolAndRelay(std::string& err_string, bool relay) +bool CWallet::SubmitTxMemoryPoolAndRelay(const CWalletTx& wtx, std::string& err_string, bool relay) const { // Can't relay if wallet is not broadcasting - if (!pwallet->GetBroadcastTransactions()) return false; + if (!GetBroadcastTransactions()) return false; // Don't relay abandoned transactions - if (isAbandoned()) return false; + if (wtx.isAbandoned()) return false; // Don't try to submit coinbase transactions. These would fail anyway but would // cause log spam. - if (IsCoinBase()) return false; + if (wtx.IsCoinBase()) return false; // Don't try to submit conflicted or confirmed transactions. - if (GetDepthInMainChain() != 0) return false; + if (GetTxDepthInMainChain(wtx) != 0) return false; // Submit transaction to mempool for relay - pwallet->WalletLogPrintf("Submitting wtx %s to mempool for relay\n", GetHash().ToString()); + WalletLogPrintf("Submitting wtx %s to mempool for relay\n", wtx.GetHash().ToString()); // We must set fInMempool here - while it will be re-set to true by the // entered-mempool callback, if we did not there would be a race where a // user could call sendmoney in a loop and hit spurious out of funds errors @@ -1735,18 +1736,17 @@ bool CWalletTx::SubmitMemoryPoolAndRelay(std::string& err_string, bool relay) // Irrespective of the failure reason, un-marking fInMempool // out-of-order is incorrect - it should be unmarked when // TransactionRemovedFromMempool fires. - bool ret = pwallet->chain().broadcastTransaction(tx, pwallet->m_default_max_tx_fee, relay, err_string); - fInMempool |= ret; + bool ret = chain().broadcastTransaction(wtx.tx, m_default_max_tx_fee, relay, err_string); + wtx.fInMempool |= ret; return ret; } -std::set<uint256> CWalletTx::GetConflicts() const +std::set<uint256> CWallet::GetTxConflicts(const CWalletTx& wtx) const { std::set<uint256> result; - if (pwallet != nullptr) { - uint256 myHash = GetHash(); - result = pwallet->GetConflicts(myHash); + uint256 myHash = wtx.GetHash(); + result = GetConflicts(myHash); result.erase(myHash); } return result; @@ -1784,11 +1784,11 @@ void CWallet::ResendWalletTransactions() for (std::pair<const uint256, CWalletTx>& item : mapWallet) { CWalletTx& wtx = item.second; // Attempt to rebroadcast all txes more than 5 minutes older than - // the last block. SubmitMemoryPoolAndRelay() will not rebroadcast + // the last block. SubmitTxMemoryPoolAndRelay() will not rebroadcast // any confirmed or conflicting txs. if (wtx.nTimeReceived > m_best_block_time - 5 * 60) continue; std::string unused_err_string; - if (wtx.SubmitMemoryPoolAndRelay(unused_err_string, true)) ++submitted_tx_count; + if (SubmitTxMemoryPoolAndRelay(wtx, unused_err_string, true)) ++submitted_tx_count; } } // cs_wallet @@ -1976,7 +1976,7 @@ void CWallet::CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::ve } std::string err_string; - if (!wtx.SubmitMemoryPoolAndRelay(err_string, true)) { + if (!SubmitTxMemoryPoolAndRelay(wtx, err_string, true)) { WalletLogPrintf("CommitTransaction(): Transaction cannot be broadcast immediately, %s\n", err_string); // TODO: if we expect the failure to be long term or permanent, instead delete wtx from the wallet and return failure. } @@ -2509,6 +2509,7 @@ std::unique_ptr<WalletDatabase> MakeWalletDatabase(const std::string& name, cons std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::string& name, std::unique_ptr<WalletDatabase> database, uint64_t wallet_creation_flags, bilingual_str& error, std::vector<bilingual_str>& warnings) { interfaces::Chain* chain = context.chain; + ArgsManager& args = *Assert(context.args); const std::string& walletFile = database->Filename(); int64_t nStart = GetTimeMillis(); @@ -2590,28 +2591,28 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri } } - if (!gArgs.GetArg("-addresstype", "").empty()) { - std::optional<OutputType> parsed = ParseOutputType(gArgs.GetArg("-addresstype", "")); + if (!args.GetArg("-addresstype", "").empty()) { + std::optional<OutputType> parsed = ParseOutputType(args.GetArg("-addresstype", "")); if (!parsed) { - error = strprintf(_("Unknown address type '%s'"), gArgs.GetArg("-addresstype", "")); + error = strprintf(_("Unknown address type '%s'"), args.GetArg("-addresstype", "")); return nullptr; } walletInstance->m_default_address_type = parsed.value(); } - if (!gArgs.GetArg("-changetype", "").empty()) { - std::optional<OutputType> parsed = ParseOutputType(gArgs.GetArg("-changetype", "")); + if (!args.GetArg("-changetype", "").empty()) { + std::optional<OutputType> parsed = ParseOutputType(args.GetArg("-changetype", "")); if (!parsed) { - error = strprintf(_("Unknown change type '%s'"), gArgs.GetArg("-changetype", "")); + error = strprintf(_("Unknown change type '%s'"), args.GetArg("-changetype", "")); return nullptr; } walletInstance->m_default_change_type = parsed.value(); } - if (gArgs.IsArgSet("-mintxfee")) { - std::optional<CAmount> min_tx_fee = ParseMoney(gArgs.GetArg("-mintxfee", "")); + if (args.IsArgSet("-mintxfee")) { + std::optional<CAmount> min_tx_fee = ParseMoney(args.GetArg("-mintxfee", "")); if (!min_tx_fee || min_tx_fee.value() == 0) { - error = AmountErrMsg("mintxfee", gArgs.GetArg("-mintxfee", "")); + error = AmountErrMsg("mintxfee", args.GetArg("-mintxfee", "")); return nullptr; } else if (min_tx_fee.value() > HIGH_TX_FEE_PER_KB) { warnings.push_back(AmountHighWarn("-mintxfee") + Untranslated(" ") + @@ -2621,8 +2622,8 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri walletInstance->m_min_fee = CFeeRate{min_tx_fee.value()}; } - if (gArgs.IsArgSet("-maxapsfee")) { - const std::string max_aps_fee{gArgs.GetArg("-maxapsfee", "")}; + if (args.IsArgSet("-maxapsfee")) { + const std::string max_aps_fee{args.GetArg("-maxapsfee", "")}; if (max_aps_fee == "-1") { walletInstance->m_max_aps_fee = -1; } else if (std::optional<CAmount> max_fee = ParseMoney(max_aps_fee)) { @@ -2637,10 +2638,10 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri } } - if (gArgs.IsArgSet("-fallbackfee")) { - std::optional<CAmount> fallback_fee = ParseMoney(gArgs.GetArg("-fallbackfee", "")); + if (args.IsArgSet("-fallbackfee")) { + std::optional<CAmount> fallback_fee = ParseMoney(args.GetArg("-fallbackfee", "")); if (!fallback_fee) { - error = strprintf(_("Invalid amount for -fallbackfee=<amount>: '%s'"), gArgs.GetArg("-fallbackfee", "")); + error = strprintf(_("Invalid amount for -fallbackfee=<amount>: '%s'"), args.GetArg("-fallbackfee", "")); return nullptr; } else if (fallback_fee.value() > HIGH_TX_FEE_PER_KB) { warnings.push_back(AmountHighWarn("-fallbackfee") + Untranslated(" ") + @@ -2652,10 +2653,10 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri // Disable fallback fee in case value was set to 0, enable if non-null value walletInstance->m_allow_fallback_fee = walletInstance->m_fallback_fee.GetFeePerK() != 0; - if (gArgs.IsArgSet("-discardfee")) { - std::optional<CAmount> discard_fee = ParseMoney(gArgs.GetArg("-discardfee", "")); + if (args.IsArgSet("-discardfee")) { + std::optional<CAmount> discard_fee = ParseMoney(args.GetArg("-discardfee", "")); if (!discard_fee) { - error = strprintf(_("Invalid amount for -discardfee=<amount>: '%s'"), gArgs.GetArg("-discardfee", "")); + error = strprintf(_("Invalid amount for -discardfee=<amount>: '%s'"), args.GetArg("-discardfee", "")); return nullptr; } else if (discard_fee.value() > HIGH_TX_FEE_PER_KB) { warnings.push_back(AmountHighWarn("-discardfee") + Untranslated(" ") + @@ -2664,10 +2665,10 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri walletInstance->m_discard_rate = CFeeRate{discard_fee.value()}; } - if (gArgs.IsArgSet("-paytxfee")) { - std::optional<CAmount> pay_tx_fee = ParseMoney(gArgs.GetArg("-paytxfee", "")); + if (args.IsArgSet("-paytxfee")) { + std::optional<CAmount> pay_tx_fee = ParseMoney(args.GetArg("-paytxfee", "")); if (!pay_tx_fee) { - error = AmountErrMsg("paytxfee", gArgs.GetArg("-paytxfee", "")); + error = AmountErrMsg("paytxfee", args.GetArg("-paytxfee", "")); return nullptr; } else if (pay_tx_fee.value() > HIGH_TX_FEE_PER_KB) { warnings.push_back(AmountHighWarn("-paytxfee") + Untranslated(" ") + @@ -2678,15 +2679,15 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri if (chain && walletInstance->m_pay_tx_fee < chain->relayMinFee()) { error = strprintf(_("Invalid amount for -paytxfee=<amount>: '%s' (must be at least %s)"), - gArgs.GetArg("-paytxfee", ""), chain->relayMinFee().ToString()); + args.GetArg("-paytxfee", ""), chain->relayMinFee().ToString()); return nullptr; } } - if (gArgs.IsArgSet("-maxtxfee")) { - std::optional<CAmount> max_fee = ParseMoney(gArgs.GetArg("-maxtxfee", "")); + if (args.IsArgSet("-maxtxfee")) { + std::optional<CAmount> max_fee = ParseMoney(args.GetArg("-maxtxfee", "")); if (!max_fee) { - error = AmountErrMsg("maxtxfee", gArgs.GetArg("-maxtxfee", "")); + error = AmountErrMsg("maxtxfee", args.GetArg("-maxtxfee", "")); return nullptr; } else if (max_fee.value() > HIGH_MAX_TX_FEE) { warnings.push_back(_("-maxtxfee is set very high! Fees this large could be paid on a single transaction.")); @@ -2694,21 +2695,30 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri if (chain && CFeeRate{max_fee.value(), 1000} < chain->relayMinFee()) { error = strprintf(_("Invalid amount for -maxtxfee=<amount>: '%s' (must be at least the minrelay fee of %s to prevent stuck transactions)"), - gArgs.GetArg("-maxtxfee", ""), chain->relayMinFee().ToString()); + args.GetArg("-maxtxfee", ""), chain->relayMinFee().ToString()); return nullptr; } walletInstance->m_default_max_tx_fee = max_fee.value(); } + if (gArgs.IsArgSet("-consolidatefeerate")) { + if (std::optional<CAmount> consolidate_feerate = ParseMoney(gArgs.GetArg("-consolidatefeerate", ""))) { + walletInstance->m_consolidate_feerate = CFeeRate(*consolidate_feerate); + } else { + error = AmountErrMsg("consolidatefeerate", gArgs.GetArg("-consolidatefeerate", "")); + return nullptr; + } + } + if (chain && chain->relayMinFee().GetFeePerK() > HIGH_TX_FEE_PER_KB) { warnings.push_back(AmountHighWarn("-minrelaytxfee") + Untranslated(" ") + _("The wallet will avoid paying less than the minimum relay fee.")); } - walletInstance->m_confirm_target = gArgs.GetArg("-txconfirmtarget", DEFAULT_TX_CONFIRM_TARGET); - walletInstance->m_spend_zero_conf_change = gArgs.GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE); - walletInstance->m_signal_rbf = gArgs.GetBoolArg("-walletrbf", DEFAULT_WALLET_RBF); + walletInstance->m_confirm_target = args.GetArg("-txconfirmtarget", DEFAULT_TX_CONFIRM_TARGET); + walletInstance->m_spend_zero_conf_change = args.GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE); + walletInstance->m_signal_rbf = args.GetBoolArg("-walletrbf", DEFAULT_WALLET_RBF); walletInstance->WalletLogPrintf("Wallet completed loading in %15dms\n", GetTimeMillis() - nStart); @@ -2728,7 +2738,7 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri } } - walletInstance->SetBroadcastTransactions(gArgs.GetBoolArg("-walletbroadcast", DEFAULT_WALLETBROADCAST)); + walletInstance->SetBroadcastTransactions(args.GetBoolArg("-walletbroadcast", DEFAULT_WALLETBROADCAST)); { walletInstance->WalletLogPrintf("setKeyPool.size() = %u\n", walletInstance->GetKeyPoolSize()); @@ -2900,28 +2910,27 @@ CKeyPool::CKeyPool(const CPubKey& vchPubKeyIn, bool internalIn) m_pre_split = false; } -int CWalletTx::GetDepthInMainChain() const +int CWallet::GetTxDepthInMainChain(const CWalletTx& wtx) const { - assert(pwallet != nullptr); - AssertLockHeld(pwallet->cs_wallet); - if (isUnconfirmed() || isAbandoned()) return 0; + AssertLockHeld(cs_wallet); + if (wtx.isUnconfirmed() || wtx.isAbandoned()) return 0; - return (pwallet->GetLastBlockHeight() - m_confirm.block_height + 1) * (isConflicted() ? -1 : 1); + return (GetLastBlockHeight() - wtx.m_confirm.block_height + 1) * (wtx.isConflicted() ? -1 : 1); } -int CWalletTx::GetBlocksToMaturity() const +int CWallet::GetTxBlocksToMaturity(const CWalletTx& wtx) const { - if (!IsCoinBase()) + if (!wtx.IsCoinBase()) return 0; - int chain_depth = GetDepthInMainChain(); + int chain_depth = GetTxDepthInMainChain(wtx); assert(chain_depth >= 0); // coinbase tx should not be conflicted return std::max(0, (COINBASE_MATURITY+1) - chain_depth); } -bool CWalletTx::IsImmatureCoinBase() const +bool CWallet::IsTxImmatureCoinBase(const CWalletTx& wtx) const { // note GetBlocksToMaturity is 0 for non-coinbase tx - return GetBlocksToMaturity() > 0; + return GetTxBlocksToMaturity(wtx) > 0; } bool CWallet::IsCrypted() const diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index a1bbf66136..fbeec2aa30 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -21,9 +21,7 @@ #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> @@ -73,6 +71,8 @@ static const CAmount DEFAULT_FALLBACK_FEE = 0; static const CAmount DEFAULT_DISCARD_FEE = 10000; //! -mintxfee default static const CAmount DEFAULT_TRANSACTION_MINFEE = 1000; +//! -consolidatefeerate default +static const CAmount DEFAULT_CONSOLIDATE_FEERATE{10000}; // 10 sat/vbyte /** * maximum fee increase allowed to do partial spend avoidance, even for nodes with this feature disabled by default * @@ -329,8 +329,6 @@ private: // 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 @@ -351,17 +349,6 @@ public: 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; } @@ -417,39 +404,40 @@ public: 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); } + // 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> GetTxConflicts(const CWalletTx& wtx) const NO_THREAD_SAFETY_ANALYSIS; /** - * populate vCoins with vector of available COutputs. + * 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 */ - 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); + // 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 GetTxDepthInMainChain(const CWalletTx& wtx) const NO_THREAD_SAFETY_ANALYSIS; + bool IsTxInMainChain(const CWalletTx& wtx) const { return GetTxDepthInMainChain(wtx) > 0; } /** - * Return list of available coins and locked coins grouped by non-change output address. + * @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 */ - std::map<CTxDestination, std::vector<COutput>> ListCoins() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + int GetTxBlocksToMaturity(const CWalletTx& wtx) const; + bool IsTxImmatureCoinBase(const CWalletTx& wtx) const; - /** - * 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; + //! 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); } bool IsSpent(const uint256& hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); @@ -457,8 +445,6 @@ public: 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); @@ -544,24 +530,9 @@ public: 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. */ @@ -589,12 +560,6 @@ public: 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. @@ -605,6 +570,9 @@ public: */ void CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::vector<std::pair<std::string, std::string>> orderForm); + /** Pass this transaction to node for mempool insertion and relay to peers if flag set to true */ + bool SubmitTxMemoryPoolAndRelay(const CWalletTx& wtx, std::string& err_string, bool relay) const; + bool DummySignTx(CMutableTransaction &txNew, const std::set<CTxOut> &txouts, bool use_max_sig = false) const { std::vector<CTxOut> v_txouts(txouts.size()); @@ -638,6 +606,12 @@ public: * output itself, just drop it to fees. */ CFeeRate m_discard_rate{DEFAULT_DISCARD_FEE}; + /** When the actual feerate is less than the consolidate feerate, we will tend to make transactions which + * consolidate inputs. When the actual feerate is greater than the consolidate feerate, we will tend to make + * transactions which have the lowest fees. + */ + CFeeRate m_consolidate_feerate{DEFAULT_CONSOLIDATE_FEERATE}; + /** 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}; @@ -656,9 +630,6 @@ public: 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; /** @@ -672,25 +643,16 @@ public: 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(); @@ -957,18 +919,6 @@ public: } }; -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); diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 2fabe65a93..03464cd2c8 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -954,7 +954,7 @@ DBErrors WalletBatch::FindWalletTx(std::vector<uint256>& vTxHash, std::list<CWal uint256 hash; ssKey >> hash; vTxHash.push_back(hash); - vWtx.emplace_back(nullptr /* wallet */, nullptr /* tx */); + vWtx.emplace_back(nullptr /* tx */); ssValue >> vWtx.back(); } } |