diff --git a/CMakeLists.txt b/CMakeLists.txt index 87a9aa1628d1..e2426055b91c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -427,6 +427,7 @@ set(UTIL_SOURCES ./src/threadinterrupt.cpp ./src/arith_uint256.cpp ./src/uint256.cpp + ./src/util/asmap.cpp ./src/util/threadnames.cpp ./src/util/blockstatecatcher.h ./src/util/system.cpp diff --git a/contrib/devtools/gen-json-headers.sh b/contrib/devtools/hexdump_util.sh similarity index 100% rename from contrib/devtools/gen-json-headers.sh rename to contrib/devtools/hexdump_util.sh diff --git a/src/Makefile.am b/src/Makefile.am index 3301d87f3f0e..56deb76576ed 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -293,6 +293,7 @@ BITCOIN_CORE_H = \ guiinterfaceutil.h \ uint256.h \ undo.h \ + util/asmap.h \ util/blockstatecatcher.h \ util/system.h \ util/macros.h \ @@ -561,6 +562,7 @@ libbitcoin_util_a_SOURCES = \ support/lockedpool.cpp \ sync.cpp \ threadinterrupt.cpp \ + util/asmap.cpp \ uint256.cpp \ util/system.cpp \ utilmoneystr.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 1e49626a9b30..b48247b41464 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -60,7 +60,8 @@ JSON_TEST_FILES = \ test/data/merkle_commitments_sapling.json \ test/data/sapling_key_components.json -RAW_TEST_FILES = +RAW_TEST_FILES = \ + test/data/asmap.raw GENERATED_TEST_FILES = $(JSON_TEST_FILES:.json=.json.h) $(RAW_TEST_FILES:.raw=.raw.h) @@ -423,3 +424,12 @@ check-local: check-sapling check-standard echo "};};"; \ } > "$@.new" && mv -f "$@.new" "$@" @echo "Generated $@" + +%.raw.h: %.raw + @$(MKDIR_P) $(@D) + @{ \ + echo "static unsigned const char $(*F)_raw[] = {" && \ + $(HEXDUMP) -v -e '8/1 "0x%02x, "' -e '"\n"' $< | $(SED) -e 's/0x ,//g' && \ + echo "};"; \ + } > "$@.new" && mv -f "$@.new" "$@" + @echo "Generated $@" diff --git a/src/addrdb.cpp b/src/addrdb.cpp index e7c12a78c1ca..ba40deca87a9 100644 --- a/src/addrdb.cpp +++ b/src/addrdb.cpp @@ -15,6 +15,8 @@ #include "tinyformat.h" #include "util/system.h" +#include + namespace { template @@ -37,7 +39,7 @@ template bool SerializeFileDB(const std::string& prefix, const fs::path& path, const Data& data) { // Generate random temporary filename - unsigned short randv = 0; + uint16_t randv = 0; GetRandBytes((unsigned char*)&randv, sizeof(randv)); std::string tmpfn = strprintf("%s.%04x", prefix, randv); diff --git a/src/addrman.cpp b/src/addrman.cpp index 62c016aa8321..de1d81b8e0b0 100644 --- a/src/addrman.cpp +++ b/src/addrman.cpp @@ -7,23 +7,30 @@ #include "addrman.h" #include "hash.h" -#include "serialize.h" #include "streams.h" +#include "logging.h" +#include "serialize.h" -int CAddrInfo::GetTriedBucket(const uint256& nKey) const +int CAddrInfo::GetTriedBucket(const uint256& nKey, const std::vector &asmap) const { uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) << nKey << GetKey()).GetHash().GetCheapHash(); - uint64_t hash2 = (CHashWriter(SER_GETHASH, 0) << nKey << GetGroup() << (hash1 % ADDRMAN_TRIED_BUCKETS_PER_GROUP)).GetHash().GetCheapHash(); - return hash2 % ADDRMAN_TRIED_BUCKET_COUNT; + uint64_t hash2 = (CHashWriter(SER_GETHASH, 0) << nKey << GetGroup(asmap) << (hash1 % ADDRMAN_TRIED_BUCKETS_PER_GROUP)).GetHash().GetCheapHash(); + int tried_bucket = hash2 % ADDRMAN_TRIED_BUCKET_COUNT; + uint32_t mapped_as = GetMappedAS(asmap); + LogPrint(BCLog::NET, "IP %s mapped to AS%i belongs to tried bucket %i\n", ToStringIP(), mapped_as, tried_bucket); + return tried_bucket; } -int CAddrInfo::GetNewBucket(const uint256& nKey, const CNetAddr& src) const +int CAddrInfo::GetNewBucket(const uint256& nKey, const CNetAddr& src, const std::vector &asmap) const { - std::vector vchSourceGroupKey = src.GetGroup(); - uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) << nKey << GetGroup() << vchSourceGroupKey).GetHash().GetCheapHash(); + std::vector vchSourceGroupKey = src.GetGroup(asmap); + uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) << nKey << GetGroup(asmap) << vchSourceGroupKey).GetHash().GetCheapHash(); uint64_t hash2 = (CHashWriter(SER_GETHASH, 0) << nKey << vchSourceGroupKey << (hash1 % ADDRMAN_NEW_BUCKETS_PER_SOURCE_GROUP)).GetHash().GetCheapHash(); - return hash2 % ADDRMAN_NEW_BUCKET_COUNT; + int new_bucket = hash2 % ADDRMAN_NEW_BUCKET_COUNT; + uint32_t mapped_as = GetMappedAS(asmap); + LogPrint(BCLog::NET, "IP %s mapped to AS%i belongs to new bucket %i\n", ToStringIP(), mapped_as, new_bucket); + return new_bucket; } int CAddrInfo::GetBucketPosition(const uint256& nKey, bool fNew, int nBucket) const @@ -156,7 +163,7 @@ void CAddrMan::MakeTried(CAddrInfo& info, int nId) assert(info.nRefCount == 0); // which tried bucket to move the entry to - int nKBucket = info.GetTriedBucket(nKey); + int nKBucket = info.GetTriedBucket(nKey, m_asmap); int nKBucketPos = info.GetBucketPosition(nKey, false, nKBucket); // first make space to add it (the existing tried entry there is moved to new, deleting whatever is there). @@ -172,7 +179,7 @@ void CAddrMan::MakeTried(CAddrInfo& info, int nId) nTried--; // find which new bucket it belongs to - int nUBucket = infoOld.GetNewBucket(nKey); + int nUBucket = infoOld.GetNewBucket(nKey, m_asmap); int nUBucketPos = infoOld.GetBucketPosition(nKey, true, nUBucket); ClearNew(nUBucket, nUBucketPos); assert(vvNew[nUBucket][nUBucketPos] == -1); @@ -236,7 +243,7 @@ void CAddrMan::Good_(const CService& addr, bool test_before_evict, int64_t nTime return; // which tried bucket to move the entry to - int tried_bucket = info.GetTriedBucket(nKey); + int tried_bucket = info.GetTriedBucket(nKey, m_asmap); int tried_bucket_pos = info.GetBucketPosition(nKey, false, tried_bucket); // Will moving this address into tried evict another entry? @@ -304,7 +311,7 @@ bool CAddrMan::Add_(const CAddress& addr, const CNetAddr& source, int64_t nTimeP fNew = true; } - int nUBucket = pinfo->GetNewBucket(nKey, source); + int nUBucket = pinfo->GetNewBucket(nKey, source, m_asmap); int nUBucketPos = pinfo->GetBucketPosition(nKey, true, nUBucket); if (vvNew[nUBucket][nUBucketPos] != nId) { bool fInsert = vvNew[nUBucket][nUBucketPos] == -1; @@ -438,15 +445,15 @@ int CAddrMan::Check_() for (int n = 0; n < ADDRMAN_TRIED_BUCKET_COUNT; n++) { for (int i = 0; i < ADDRMAN_BUCKET_SIZE; i++) { - if (vvTried[n][i] != -1) { - if (!setTried.count(vvTried[n][i])) - return -11; - if (mapInfo[vvTried[n][i]].GetTriedBucket(nKey) != n) - return -17; - if (mapInfo[vvTried[n][i]].GetBucketPosition(nKey, false, n) != i) - return -18; - setTried.erase(vvTried[n][i]); - } + if (vvTried[n][i] != -1) { + if (!setTried.count(vvTried[n][i])) + return -11; + if (mapInfo[vvTried[n][i]].GetTriedBucket(nKey, m_asmap) != n) + return -17; + if (mapInfo[vvTried[n][i]].GetBucketPosition(nKey, false, n) != i) + return -18; + setTried.erase(vvTried[n][i]); + } } } @@ -547,7 +554,7 @@ void CAddrMan::ResolveCollisions_() CAddrInfo& info_new = mapInfo[id_new]; // Which tried bucket to move the entry to. - int tried_bucket = info_new.GetTriedBucket(nKey); + int tried_bucket = info_new.GetTriedBucket(nKey, m_asmap); int tried_bucket_pos = info_new.GetBucketPosition(nKey, false, tried_bucket); if (!info_new.IsValid()) { // id_new may no longer map to a valid address erase_collision = true; @@ -611,10 +618,33 @@ CAddrInfo CAddrMan::SelectTriedCollision_() CAddrInfo& newInfo = mapInfo[id_new]; // which tried bucket to move the entry to - int tried_bucket = newInfo.GetTriedBucket(nKey); + int tried_bucket = newInfo.GetTriedBucket(nKey, m_asmap); int tried_bucket_pos = newInfo.GetBucketPosition(nKey, false, tried_bucket); int id_old = vvTried[tried_bucket][tried_bucket_pos]; return mapInfo[id_old]; } + +std::vector CAddrMan::DecodeAsmap(fs::path path) +{ + std::vector bits; + FILE *filestr = fsbridge::fopen(path, "rb"); + CAutoFile file(filestr, SER_DISK, CLIENT_VERSION); + if (file.IsNull()) { + LogPrintf("Failed to open asmap file from disk\n"); + return bits; + } + fseek(filestr, 0, SEEK_END); + int length = ftell(filestr); + LogPrintf("Opened asmap file %s (%d bytes) from disk\n", path, length); + fseek(filestr, 0, SEEK_SET); + char cur_byte; + for (int i = 0; i < length; ++i) { + file >> cur_byte; + for (int bit = 0; bit < 8; ++bit) { + bits.push_back((cur_byte >> bit) & 1); + } + } + return bits; +} diff --git a/src/addrman.h b/src/addrman.h index a36c8774205b..96d340e826d8 100644 --- a/src/addrman.h +++ b/src/addrman.h @@ -7,6 +7,7 @@ #ifndef BITCOIN_ADDRMAN_H #define BITCOIN_ADDRMAN_H +#include "clientversion.h" #include "netaddress.h" #include "protocol.h" #include "random.h" @@ -14,9 +15,13 @@ #include "timedata.h" #include "util/system.h" +#include +#include +#include #include #include #include +#include #include /** @@ -24,33 +29,31 @@ */ class CAddrInfo : public CAddress { - - public: //! last try whatsoever by us (memory only) - int64_t nLastTry; + int64_t nLastTry{0}; //! last counted attempt (memory only) - int64_t nLastCountAttempt; + int64_t nLastCountAttempt{0}; private: //! where knowledge about this address first came from CNetAddr source; //! last successful connection by us - int64_t nLastSuccess; + int64_t nLastSuccess{0}; //! connection attempts since last successful attempt - int nAttempts; + int nAttempts{0}; //! reference count in new sets (memory only) - int nRefCount; + int nRefCount{0}; //! in tried set? (memory only) - bool fInTried; + bool fInTried{false}; //! position in vRandom - int nRandomPos; + int nRandomPos{-1}; friend class CAddrMan; @@ -62,37 +65,24 @@ class CAddrInfo : public CAddress READWRITE(obj.source, obj.nLastSuccess, obj.nAttempts); } - void Init() - { - nLastSuccess = 0; - nLastTry = 0; - nLastCountAttempt = 0; - nAttempts = 0; - nRefCount = 0; - fInTried = false; - nRandomPos = -1; - } - CAddrInfo(const CAddress& addrIn, const CNetAddr& addrSource) : CAddress(addrIn), source(addrSource) { - Init(); } CAddrInfo() : CAddress(), source() { - Init(); } //! Calculate in which "tried" bucket this entry belongs - int GetTriedBucket(const uint256& nKey) const; + int GetTriedBucket(const uint256& nKey, const std::vector& asmap) const; //! Calculate in which "new" bucket this entry belongs, given a certain source - int GetNewBucket(const uint256& nKey, const CNetAddr& src) const; + int GetNewBucket(const uint256& nKey, const CNetAddr& src, const std::vector& asmap) const; //! Calculate in which "new" bucket this entry belongs, using its default source - int GetNewBucket(const uint256& nKey) const + int GetNewBucket(const uint256& nKey, const std::vector& asmap) const { - return GetNewBucket(nKey, source); + return GetNewBucket(nKey, source, asmap); } //! Calculate in which position of a bucket to store this entry. @@ -108,15 +98,15 @@ class CAddrInfo : public CAddress /** Stochastic address manager * * Design goals: - * * Keep the address tables in-memory, and asynchronously dump the entire to able in peers.dat. + * * Keep the address tables in-memory, and asynchronously dump the entire table to peers.dat. * * Make sure no (localized) attacker can fill the entire table with his nodes/addresses. * * To that end: * * Addresses are organized into buckets. - * * Address that have not yet been tried go into 1024 "new" buckets. - * * Based on the address range (/16 for IPv4) of source of the information, 64 buckets are selected at random - * * The actual bucket is chosen from one of these, based on the range the address itself is located. - * * One single address can occur in up to 8 different buckets, to increase selection chances for addresses that + * * Addresses that have not yet been tried go into 1024 "new" buckets. + * * Based on the address range (/16 for IPv4) of the source of information, 64 buckets are selected at random. + * * The actual bucket is chosen from one of these, based on the range in which the address itself is located. + * * One single address can occur in up to 8 different buckets to increase selection chances for addresses that * are seen frequently. The chance for increasing this multiplicity decreases exponentially. * * When adding a new address to a full bucket, a randomly chosen entry (with a bias favoring less recently seen * ones) is removed from it first. @@ -186,6 +176,7 @@ static const int64_t ADDRMAN_TEST_WINDOW = 40*60; // 40 minutes */ class CAddrMan { +friend class CAddrManTest; protected: //! critical section to protect the inner data structures mutable RecursiveMutex cs; @@ -218,7 +209,7 @@ class CAddrMan //! last time Good was called (memory only) int64_t nLastGood GUARDED_BY(cs); - //! Holds addrs inserted into tried table that collide with existing entries. Test-before-evict discpline used to resolve these collisions. + //! Holds addrs inserted into tried table that collide with existing entries. Test-before-evict discipline used to resolve these collisions. std::set m_tried_collisions; protected: @@ -280,9 +271,29 @@ class CAddrMan void SetServices_(const CService& addr, ServiceFlags nServices) EXCLUSIVE_LOCKS_REQUIRED(cs); public: + // Compressed IP->ASN mapping, loaded from a file when a node starts. + // Should be always empty if no file was provided. + // This mapping is then used for bucketing nodes in Addrman. + // + // If asmap is provided, nodes will be bucketed by + // AS they belong to, in order to make impossible for a node + // to connect to several nodes hosted in a single AS. + // This is done in response to Erebus attack, but also to generally + // diversify the connections every node creates, + // especially useful when a large fraction of nodes + // operate under a couple of cloud providers. + // + // If a new asmap was provided, the existing records + // would be re-bucketed accordingly. + std::vector m_asmap; + + // Read asmap from provided binary file + static std::vector DecodeAsmap(fs::path path); + + /** * serialized format: - * * version byte (currently 1) + * * version byte (1 for pre-asmap files, 2 for files including asmap version) * * 0x20 + nKey (serialized as if it were a vector, for backward compatibility) * * nNew * * nTried @@ -300,7 +311,7 @@ class CAddrMan * Notice that vvTried, mapAddr and vVector are never encoded explicitly; * they are instead reconstructed from the other information. * - * vvNew is serialized, but only used if ADDRMAN_UNKOWN_BUCKET_COUNT didn't change, + * vvNew is serialized, but only used if ADDRMAN_UNKNOWN_BUCKET_COUNT didn't change, * otherwise it is reconstructed as well. * * This format is more complex, but significantly smaller (at most 1.5 MiB), and supports @@ -314,7 +325,7 @@ class CAddrMan { LOCK(cs); - unsigned char nVersion = 1; + unsigned char nVersion = 2; s << nVersion; s << ((unsigned char)32); s << nKey; @@ -327,7 +338,7 @@ class CAddrMan int nIds = 0; for (const auto& entry : mapInfo) { mapUnkIds[entry.first] = nIds; - const CAddrInfo &info = entry.second; + const CAddrInfo& info = entry.second; if (info.nRefCount) { assert(nIds != nNew); // this means nNew was wrong, oh ow s << info; @@ -336,7 +347,7 @@ class CAddrMan } nIds = 0; for (const auto& entry : mapInfo) { - const CAddrInfo &info = entry.second; + const CAddrInfo& info = entry.second; if (info.fInTried) { assert(nIds != nTried); // this means nTried was wrong, oh ow s << info; @@ -357,6 +368,13 @@ class CAddrMan } } } + // Store asmap version after bucket entries so that it + // can be ignored by older clients for backward compatibility. + uint256 asmap_version; + if (m_asmap.size() != 0) { + asmap_version = SerializeHash(m_asmap); + } + s << asmap_version; } template @@ -365,7 +383,6 @@ class CAddrMan LOCK(cs); Clear(); - unsigned char nVersion; s >> nVersion; unsigned char nKeySize; @@ -380,6 +397,14 @@ class CAddrMan nUBuckets ^= (1 << 30); } + if (nNew > ADDRMAN_NEW_BUCKET_COUNT * ADDRMAN_BUCKET_SIZE) { + throw std::ios_base::failure("Corrupt CAddrMan serialization, nNew exceeds limit."); + } + + if (nTried > ADDRMAN_TRIED_BUCKET_COUNT * ADDRMAN_BUCKET_SIZE) { + throw std::ios_base::failure("Corrupt CAddrMan serialization, nTried exceeds limit."); + } + // Deserialize entries from the new table. for (int n = 0; n < nNew; n++) { CAddrInfo& info = mapInfo[n]; @@ -387,16 +412,6 @@ class CAddrMan mapAddr[info] = n; info.nRandomPos = vRandom.size(); vRandom.push_back(n); - if (nVersion != 1 || nUBuckets != ADDRMAN_NEW_BUCKET_COUNT) { - // In case the new table data cannot be used (nVersion unknown, or bucket count wrong), - // immediately try to give them a reference based on their primary source address. - int nUBucket = info.GetNewBucket(nKey); - int nUBucketPos = info.GetBucketPosition(nKey, true, nUBucket); - if (vvNew[nUBucket][nUBucketPos] == -1) { - vvNew[nUBucket][nUBucketPos] = n; - info.nRefCount++; - } - } } nIdCount = nNew; @@ -405,7 +420,7 @@ class CAddrMan for (int n = 0; n < nTried; n++) { CAddrInfo info; s >> info; - int nKBucket = info.GetTriedBucket(nKey); + int nKBucket = info.GetTriedBucket(nKey, m_asmap); int nKBucketPos = info.GetBucketPosition(nKey, false, nKBucket); if (vvTried[nKBucket][nKBucketPos] == -1) { info.nRandomPos = vRandom.size(); @@ -421,7 +436,9 @@ class CAddrMan } nTried -= nLost; - // Deserialize positions in the new table (if possible). + // Store positions in the new table buckets to apply later (if possible). + std::map entryToBucket; // Represents which entry belonged to which bucket when serializing + for (int bucket = 0; bucket < nUBuckets; bucket++) { int nSize = 0; s >> nSize; @@ -429,12 +446,38 @@ class CAddrMan int nIndex = 0; s >> nIndex; if (nIndex >= 0 && nIndex < nNew) { - CAddrInfo& info = mapInfo[nIndex]; - int nUBucketPos = info.GetBucketPosition(nKey, true, bucket); - if (nVersion == 1 && nUBuckets == ADDRMAN_NEW_BUCKET_COUNT && vvNew[bucket][nUBucketPos] == -1 && info.nRefCount < ADDRMAN_NEW_BUCKETS_PER_ADDRESS) { - info.nRefCount++; - vvNew[bucket][nUBucketPos] = nIndex; - } + entryToBucket[nIndex] = bucket; + } + } + } + + uint256 supplied_asmap_version; + if (m_asmap.size() != 0) { + supplied_asmap_version = SerializeHash(m_asmap); + } + uint256 serialized_asmap_version; + if (nVersion > 1) { + s >> serialized_asmap_version; + } + + for (int n = 0; n < nNew; n++) { + CAddrInfo &info = mapInfo[n]; + int bucket = entryToBucket[n]; + int nUBucketPos = info.GetBucketPosition(nKey, true, bucket); + if (nVersion == 2 && nUBuckets == ADDRMAN_NEW_BUCKET_COUNT && vvNew[bucket][nUBucketPos] == -1 && + info.nRefCount < ADDRMAN_NEW_BUCKETS_PER_ADDRESS && serialized_asmap_version == supplied_asmap_version) { + // Bucketing has not changed, using existing bucket positions for the new table + vvNew[bucket][nUBucketPos] = n; + info.nRefCount++; + } else { + // In case the new table data cannot be used (nVersion unknown, bucket count wrong or new asmap), + // try to give them a reference based on their primary source address. + LogPrint(BCLog::ADDRMAN, "Bucketing method was updated, re-bucketing addrman entries from disk\n"); + bucket = info.GetNewBucket(nKey, m_asmap); + nUBucketPos = info.GetBucketPosition(nKey, true, bucket); + if (vvNew[bucket][nUBucketPos] == -1) { + vvNew[bucket][nUBucketPos] = n; + info.nRefCount++; } } } @@ -457,7 +500,6 @@ class CAddrMan Check(); } - void Clear() { LOCK(cs); @@ -489,7 +531,7 @@ class CAddrMan ~CAddrMan() { - nKey = uint256(); + nKey.SetNull(); } //! Return the number of (unique) addresses in all tables. @@ -520,8 +562,9 @@ class CAddrMan Check(); fRet |= Add_(addr, source, nTimePenalty); Check(); - if (fRet) + if (fRet) { LogPrint(BCLog::ADDRMAN, "Added %s from %s: %i tried, %i new\n", addr.ToStringIPPort(), source.ToString(), nTried, nNew); + } return fRet; } @@ -534,8 +577,9 @@ class CAddrMan for (std::vector::const_iterator it = vAddr.begin(); it != vAddr.end(); it++) nAdd += Add_(*it, source, nTimePenalty) ? 1 : 0; Check(); - if (nAdd) + if (nAdd) { LogPrint(BCLog::ADDRMAN, "Added %i addresses from %s: %i tried, %i new\n", nAdd, source.ToString(), nTried, nNew); + } return nAdd > 0; } @@ -624,6 +668,7 @@ class CAddrMan SetServices_(addr, nServices); Check(); } + }; #endif // BITCOIN_ADDRMAN_H diff --git a/src/init.cpp b/src/init.cpp index 9c5ca2578b4d..b5a731f07ec7 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -52,6 +52,7 @@ #include "torcontrol.h" #include "guiinterface.h" #include "guiinterfaceutil.h" +#include "util/asmap.h" #include "util/system.h" #include "utilmoneystr.h" #include "util/threadnames.h" @@ -114,6 +115,8 @@ static EvoNotificationInterface* pEvoNotificationInterface = nullptr; #define MIN_CORE_FILEDESCRIPTORS 150 #endif +static const char* DEFAULT_ASMAP_FILENAME="ip_asn.map"; + /** Used to pass flags to the Bind() function */ enum BindFlags { BF_NONE = 0, @@ -472,6 +475,7 @@ std::string HelpMessage(HelpMessageMode mode) strUsage += HelpMessageGroup(_("Connection options:")); strUsage += HelpMessageOpt("-addnode=", _("Add a node to connect to and attempt to keep the connection open")); + strUsage += HelpMessageOpt("-asmap=", strprintf("Specify asn mapping used for bucketing of the peers (default: %s). Relative paths will be prefixed by the net-specific datadir location.", DEFAULT_ASMAP_FILENAME)); strUsage += HelpMessageOpt("-banscore=", strprintf(_("Threshold for disconnecting misbehaving peers (default: %u)"), DEFAULT_BANSCORE_THRESHOLD)); strUsage += HelpMessageOpt("-bantime=", strprintf(_("Number of seconds to keep misbehaving peers from reconnecting (default: %u)"), DEFAULT_MISBEHAVING_BANTIME)); strUsage += HelpMessageOpt("-bind=", _("Bind to given address and always listen on it. Use [host]:port notation for IPv6")); @@ -1395,7 +1399,7 @@ bool AppInitMain() // -proxy sets a proxy for all outgoing network traffic // -noproxy (or -proxy=0) as well as the empty string can be used to not set a proxy, this is the default std::string proxyArg = gArgs.GetArg("-proxy", ""); - SetLimited(NET_TOR); + SetLimited(NET_ONION); if (!proxyArg.empty() && proxyArg != "0") { CService proxyAddr; if (!Lookup(proxyArg.c_str(), proxyAddr, 9050, fNameLookup)) { @@ -1408,9 +1412,9 @@ bool AppInitMain() SetProxy(NET_IPV4, addrProxy); SetProxy(NET_IPV6, addrProxy); - SetProxy(NET_TOR, addrProxy); + SetProxy(NET_ONION, addrProxy); SetNameProxy(addrProxy); - SetLimited(NET_TOR, false); // by default, -proxy sets onion as reachable, unless -noonion later + SetLimited(NET_ONION, false); // by default, -proxy sets onion as reachable, unless -noonion later } // -onion can be used to set only a proxy for .onion, or override normal proxy for .onion addresses @@ -1419,7 +1423,7 @@ bool AppInitMain() std::string onionArg = gArgs.GetArg("-onion", ""); if (!onionArg.empty()) { if (onionArg == "0") { // Handle -noonion/-onion=0 - SetLimited(NET_TOR); // set onions as unreachable + SetLimited(NET_ONION); // set onions as unreachable } else { CService onionProxy; if (!Lookup(onionArg.c_str(), onionProxy, 9050, fNameLookup)) { @@ -1428,8 +1432,8 @@ bool AppInitMain() proxyType addrOnion = proxyType(onionProxy, proxyRandomize); if (!addrOnion.IsValid()) return UIError(strprintf(_("%s Invalid %s address or hostname: '%s'"), "isValid():", "-onion", onionArg)); - SetProxy(NET_TOR, addrOnion); - SetLimited(NET_TOR, false); + SetProxy(NET_ONION, addrOnion); + SetLimited(NET_ONION, false); } } @@ -1471,6 +1475,31 @@ bool AppInitMain() return UIError(ResolveErrMsg("externalip", strAddr)); } + // Read asmap file if configured + if (gArgs.IsArgSet("-asmap")) { + fs::path asmap_path = fs::path(gArgs.GetArg("-asmap", "")); + if (asmap_path.empty()) { + asmap_path = DEFAULT_ASMAP_FILENAME; + } + if (!asmap_path.is_absolute()) { + asmap_path = GetDataDir() / asmap_path; + } + if (!fs::exists(asmap_path)) { + UIError(strprintf(_("Could not find asmap file %s"), asmap_path)); + return false; + } + std::vector asmap = CAddrMan::DecodeAsmap(asmap_path); + if (asmap.size() == 0) { + UIError(strprintf(_("Could not parse asmap file %s"), asmap_path)); + return false; + } + const uint256 asmap_version = SerializeHash(asmap); + connman.SetAsmap(std::move(asmap)); + LogPrintf("Using asmap version %s for IP bucketing\n", asmap_version.ToString()); + } else { + LogPrintf("Using /16 prefix for IP bucketing\n"); + } + // Warn if network-specific options (-addnode, -connect, etc) are // specified in default section of config file, but not overridden // on the command line or in this network's section of the config file. @@ -1952,8 +1981,6 @@ bool AppInitMain() GenerateBitcoins(gArgs.GetBoolArg("-gen", DEFAULT_GENERATE), vpwallets[0], gArgs.GetArg("-genproclimit", DEFAULT_GENERATE_PROCLIMIT)); #endif - // ********************************************************* Step 12: finished - #ifdef ENABLE_WALLET uiInterface.InitMessage(_("Reaccepting wallet transactions...")); for (CWalletRef pwallet : vpwallets) { @@ -1965,6 +1992,8 @@ bool AppInitMain() } #endif + // ********************************************************* Step 12: finished + SetRPCWarmupFinished(); uiInterface.InitMessage(_("Done loading")); diff --git a/src/net.cpp b/src/net.cpp index efe5399db6fd..285adc795990 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -32,6 +32,8 @@ #include #endif +#include + #include // Dump addresses to peers.dat and banlist.dat every 15 minutes (900s) @@ -78,9 +80,9 @@ void CConnman::AddOneShot(const std::string& strDest) vOneShots.push_back(strDest); } -unsigned short GetListenPort() +uint16_t GetListenPort() { - return (unsigned short)(gArgs.GetArg("-port", Params().GetDefaultPort())); + return (uint16_t)(gArgs.GetArg("-port", Params().GetDefaultPort())); } // find 'best' local address for a particular peer @@ -626,10 +628,11 @@ void CNode::SetAddrLocal(const CService& addrLocalIn) { #undef X #define X(name) stats.name = name -void CNode::copyStats(CNodeStats& stats) +void CNode::copyStats(CNodeStats& stats, const std::vector& m_asmap) { stats.nodeid = this->GetId(); X(nServices); + stats.m_mapped_as = addr.GetMappedAS(m_asmap); X(nLastSend); X(nLastRecv); X(nTimeConnected); @@ -1516,9 +1519,9 @@ void CConnman::ThreadOpenConnections() std::set > setConnected; { LOCK(cs_vNodes); - for (CNode* pnode : vNodes) { + for (const CNode* pnode : vNodes) { if (!pnode->fInbound) { - setConnected.insert(pnode->addr.GetGroup()); + setConnected.insert(pnode->addr.GetGroup(addrman.m_asmap)); nOutbound++; } } @@ -1560,7 +1563,7 @@ void CConnman::ThreadOpenConnections() } // Require outbound connections, other than feelers, to be to distinct network groups - if (!fFeeler && setConnected.count(addr.GetGroup())) { + if (!fFeeler && setConnected.count(addr.GetGroup(addrman.m_asmap))) { break; } @@ -2208,7 +2211,7 @@ void CConnman::GetNodeStats(std::vector& vstats) vstats.reserve(vNodes.size()); for (CNode* pnode : vNodes) { vstats.emplace_back(); - pnode->copyStats(vstats.back()); + pnode->copyStats(vstats.back(), addrman.m_asmap); } } @@ -2476,7 +2479,7 @@ CSipHasher CConnman::GetDeterministicRandomizer(uint64_t id) uint64_t CConnman::CalculateKeyedNetGroup(const CAddress& ad) { - std::vector vchNetGroup(ad.GetGroup()); + std::vector vchNetGroup(ad.GetGroup(addrman.m_asmap)); return GetDeterministicRandomizer(RANDOMIZER_ID_NETGROUP).Write(vchNetGroup.data(), vchNetGroup.size()).Finalize(); } diff --git a/src/net.h b/src/net.h index 860f7aac9963..2ed939e91a55 100644 --- a/src/net.h +++ b/src/net.h @@ -24,8 +24,8 @@ #include "threadinterrupt.h" #include +#include #include -#include #include #include #include @@ -137,6 +137,7 @@ class CConnman NetEventsInterface* m_msgproc = nullptr; unsigned int nSendBufferMaxSize = 0; unsigned int nReceiveFloodSize = 0; + std::vector m_asmap; }; CConnman(uint64_t seed0, uint64_t seed1); ~CConnman(); @@ -269,6 +270,8 @@ class CConnman CSipHasher GetDeterministicRandomizer(uint64_t id); unsigned int GetReceiveFloodSize() const; + + void SetAsmap(std::vector asmap) { addrman.m_asmap = std::move(asmap); } private: struct ListenSocket { SOCKET socket; @@ -383,7 +386,7 @@ class CConnman }; extern std::unique_ptr g_connman; void Discover(); -unsigned short GetListenPort(); +uint16_t GetListenPort(); bool BindListenPort(const CService& bindAddr, std::string& strError, bool fWhitelisted = false); void CheckOffsetDisconnectedPeers(const CNetAddr& ip); @@ -468,6 +471,7 @@ class CNodeStats double dPingTime; double dPingWait; std::string addrLocal; + uint32_t m_mapped_as; }; @@ -777,7 +781,7 @@ class CNode void CloseSocketDisconnect(); bool DisconnectOldProtocol(int nVersionIn, int nVersionRequired); - void copyStats(CNodeStats& stats); + void copyStats(CNodeStats& stats, const std::vector& m_asmap); ServiceFlags GetLocalServices() const { diff --git a/src/netaddress.cpp b/src/netaddress.cpp index 264abaee8f88..4d92a76e1bf6 100644 --- a/src/netaddress.cpp +++ b/src/netaddress.cpp @@ -7,8 +7,11 @@ #include "netaddress.h" #include "hash.h" #include "utilstrencodings.h" +#include "util/asmap.h" #include "tinyformat.h" +#include + static const unsigned char pchIPv4[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff }; static const unsigned char pchOnionCat[] = {0xFD,0x87,0xD8,0x7E,0xEB,0x43}; @@ -171,6 +174,11 @@ bool CNetAddr::IsRFC4843() const return (GetByte(15) == 0x20 && GetByte(14) == 0x01 && GetByte(13) == 0x00 && (GetByte(12) & 0xF0) == 0x10); } +bool CNetAddr::IsHeNet() const +{ + return (GetByte(15) == 0x20 && GetByte(14) == 0x01 && GetByte(13) == 0x04 && GetByte(12) == 0x70); +} + bool CNetAddr::IsTor() const { return (memcmp(ip, pchOnionCat, sizeof(pchOnionCat)) == 0); @@ -251,7 +259,7 @@ enum Network CNetAddr::GetNetwork() const return NET_IPV4; if (IsTor()) - return NET_TOR; + return NET_ONION; return NET_IPV6; } @@ -314,69 +322,130 @@ bool CNetAddr::GetIn6Addr(struct in6_addr* pipv6Addr) const return true; } -// get canonical identifier of an address' group -// no two connections will be attempted to addresses with the same group -std::vector CNetAddr::GetGroup() const +bool CNetAddr::HasLinkedIPv4() const +{ + return IsRoutable() && (IsIPv4() || IsRFC6145() || IsRFC6052() || IsRFC3964() || IsRFC4380()); +} + +uint32_t CNetAddr::GetLinkedIPv4() const +{ + if (IsIPv4() || IsRFC6145() || IsRFC6052()) { + // IPv4, mapped IPv4, SIIT translated IPv4: the IPv4 address is the last 4 bytes of the address + return ReadBE32(ip + 12); + } else if (IsRFC3964()) { + // 6to4 tunneled IPv4: the IPv4 address is in bytes 2-6 + return ReadBE32(ip + 2); + } else if (IsRFC4380()) { + // Teredo tunneled IPv4: the IPv4 address is in the last 4 bytes of the address, but bitflipped + return ~ReadBE32(ip + 12); + } + assert(false); +} + +uint32_t CNetAddr::GetNetClass() const { + uint32_t net_class = NET_IPV6; + if (IsLocal()) { + net_class = 255; + } + if (IsInternal()) { + net_class = NET_INTERNAL; + } else if (!IsRoutable()) { + net_class = NET_UNROUTABLE; + } else if (HasLinkedIPv4()) { + net_class = NET_IPV4; + } else if (IsTor()) { + net_class = NET_ONION; + } + return net_class; +} + +uint32_t CNetAddr::GetMappedAS(const std::vector &asmap) const { + uint32_t net_class = GetNetClass(); + if (asmap.size() == 0 || (net_class != NET_IPV4 && net_class != NET_IPV6)) { + return 0; // Indicates not found, safe because AS0 is reserved per RFC7607. + } + std::vector ip_bits(128); + if (HasLinkedIPv4()) { + // For lookup, treat as if it was just an IPv4 address (pchIPv4 prefix + IPv4 bits) + for (int8_t byte_i = 0; byte_i < 12; ++byte_i) { + for (uint8_t bit_i = 0; bit_i < 8; ++bit_i) { + ip_bits[byte_i * 8 + bit_i] = (pchIPv4[byte_i] >> (7 - bit_i)) & 1; + } + } + uint32_t ipv4 = GetLinkedIPv4(); + for (int i = 0; i < 32; ++i) { + ip_bits[96 + i] = (ipv4 >> (31 - i)) & 1; + } + } else { + // Use all 128 bits of the IPv6 address otherwise + for (int8_t byte_i = 0; byte_i < 16; ++byte_i) { + uint8_t cur_byte = GetByte(15 - byte_i); + for (uint8_t bit_i = 0; bit_i < 8; ++bit_i) { + ip_bits[byte_i * 8 + bit_i] = (cur_byte >> (7 - bit_i)) & 1; + } + } + } + uint32_t mapped_as = Interpret(asmap, ip_bits); + return mapped_as; +} + +/** + * Get the canonical identifier of our network group + * + * The groups are assigned in a way where it should be costly for an attacker to + * obtain addresses with many different group identifiers, even if it is cheap + * to obtain addresses with the same identifier. + * + * @note No two connections will be attempted to addresses with the same network + * group. + */ +std::vector CNetAddr::GetGroup(const std::vector &asmap) const { std::vector vchRet; - int nClass = NET_IPV6; + uint32_t net_class = GetNetClass(); + // If non-empty asmap is supplied and the address is IPv4/IPv6, + // return ASN to be used for bucketing. + uint32_t asn = GetMappedAS(asmap); + if (asn != 0) { // Either asmap was empty, or address has non-asmappable net class (e.g. TOR). + vchRet.push_back(NET_IPV6); // IPv4 and IPv6 with same ASN should be in the same bucket + for (int i = 0; i < 4; i++) { + vchRet.push_back((asn >> (8 * i)) & 0xFF); + } + return vchRet; + } + + vchRet.push_back(net_class); int nStartByte = 0; int nBits = 16; - // all local addresses belong to the same group - if (IsLocal()) - { - nClass = 255; + if (IsLocal()) { + // all local addresses belong to the same group nBits = 0; - } - // all internal-usage addresses get their own group - if (IsInternal()) - { - nClass = NET_INTERNAL; + } else if (IsInternal()) { + // all internal-usage addresses get their own group nStartByte = sizeof(g_internal_prefix); nBits = (sizeof(ip) - sizeof(g_internal_prefix)) * 8; - } - // all other unroutable addresses belong to the same group - else if (!IsRoutable()) - { - nClass = NET_UNROUTABLE; + } else if (!IsRoutable()) { + // all other unroutable addresses belong to the same group nBits = 0; - } - // for IPv4 addresses, '1' + the 16 higher-order bits of the IP - // includes mapped IPv4, SIIT translated IPv4, and the well-known prefix - else if (IsIPv4() || IsRFC6145() || IsRFC6052()) - { - nClass = NET_IPV4; - nStartByte = 12; - } - // for 6to4 tunnelled addresses, use the encapsulated IPv4 address - else if (IsRFC3964()) - { - nClass = NET_IPV4; - nStartByte = 2; - } - // for Teredo-tunnelled IPv6 addresses, use the encapsulated IPv4 address - else if (IsRFC4380()) - { - vchRet.push_back(NET_IPV4); - vchRet.push_back(GetByte(3) ^ 0xFF); - vchRet.push_back(GetByte(2) ^ 0xFF); + } else if (HasLinkedIPv4()) { + // IPv4 addresses (and mapped IPv4 addresses) use /16 groups + uint32_t ipv4 = GetLinkedIPv4(); + vchRet.push_back((ipv4 >> 24) & 0xFF); + vchRet.push_back((ipv4 >> 16) & 0xFF); return vchRet; - } - else if (IsTor()) - { - nClass = NET_TOR; + } else if (IsTor()) { nStartByte = 6; nBits = 4; - } - // for he.net, use /36 groups - else if (GetByte(15) == 0x20 && GetByte(14) == 0x01 && GetByte(13) == 0x04 && GetByte(12) == 0x70) + } else if (IsHeNet()) { + // for he.net, use /36 groups nBits = 36; - // for the rest of the IPv6 network, use /32 groups - else + } else { + // for the rest of the IPv6 network, use /32 groups nBits = 32; + } - vchRet.push_back(nClass); + // push our ip onto vchRet byte by byte... while (nBits >= 8) { vchRet.push_back(GetByte(15 - nStartByte)); @@ -443,11 +512,11 @@ int CNetAddr::GetReachabilityFrom(const CNetAddr *paddrPartner) const case NET_IPV4: return REACH_IPV4; case NET_IPV6: return fTunnel ? REACH_IPV6_WEAK : REACH_IPV6_STRONG; // only prefer giving our IPv6 address if it's not tunnelled } - case NET_TOR: + case NET_ONION: switch(ourNet) { default: return REACH_DEFAULT; case NET_IPV4: return REACH_IPV4; // Tor users can connect to IPv4 as well - case NET_TOR: return REACH_PRIVATE; + case NET_ONION: return REACH_PRIVATE; } case NET_TEREDO: switch(ourNet) { @@ -464,7 +533,7 @@ int CNetAddr::GetReachabilityFrom(const CNetAddr *paddrPartner) const case NET_TEREDO: return REACH_TEREDO; case NET_IPV6: return REACH_IPV6_WEAK; case NET_IPV4: return REACH_IPV4; - case NET_TOR: return REACH_PRIVATE; // either from Tor, or don't care about our address + case NET_ONION: return REACH_PRIVATE; // either from Tor, or don't care about our address } } } @@ -479,15 +548,15 @@ CService::CService() Init(); } -CService::CService(const CNetAddr& cip, unsigned short portIn) : CNetAddr(cip), port(portIn) +CService::CService(const CNetAddr& cip, uint16_t portIn) : CNetAddr(cip), port(portIn) { } -CService::CService(const struct in_addr& ipv4Addr, unsigned short portIn) : CNetAddr(ipv4Addr), port(portIn) +CService::CService(const struct in_addr& ipv4Addr, uint16_t portIn) : CNetAddr(ipv4Addr), port(portIn) { } -CService::CService(const struct in6_addr& ipv6Addr, unsigned short portIn) : CNetAddr(ipv6Addr), port(portIn) +CService::CService(const struct in6_addr& ipv6Addr, uint16_t portIn) : CNetAddr(ipv6Addr), port(portIn) { } @@ -515,7 +584,7 @@ bool CService::SetSockAddr(const struct sockaddr *paddr) } } -unsigned short CService::GetPort() const +uint16_t CService::GetPort() const { return port; } @@ -594,11 +663,6 @@ std::string CService::ToString() const return ToStringIPPort(); } -void CService::SetPort(unsigned short portIn) -{ - port = portIn; -} - CSubNet::CSubNet(): valid(false) { diff --git a/src/netaddress.h b/src/netaddress.h index 316651dcf3f8..5f54c6fa843a 100644 --- a/src/netaddress.h +++ b/src/netaddress.h @@ -14,7 +14,7 @@ #include "serialize.h" #include "span.h" -#include +#include #include #include @@ -22,7 +22,7 @@ enum Network { NET_UNROUTABLE = 0, NET_IPV4, NET_IPV6, - NET_TOR, + NET_ONION, NET_INTERNAL, NET_MAX, @@ -57,7 +57,7 @@ class CNetAddr bool IsIPv4() const; // IPv4 mapped address (::FFFF:0:0/96, 0.0.0.0/0) bool IsIPv6() const; // IPv6 address (not mapped IPv4, not Tor) bool IsRFC1918() const; // IPv4 private networks (10.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12) - bool IsRFC2544() const; // IPv4 inter-network communcations (192.18.0.0/15) + bool IsRFC2544() const; // IPv4 inter-network communications (198.18.0.0/15) bool IsRFC6598() const; // IPv4 ISP-level NAT (100.64.0.0/10) bool IsRFC5737() const; // IPv4 documentation addresses (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24) bool IsRFC3849() const; // IPv6 documentation address (2001:0DB8::/32) @@ -69,6 +69,7 @@ class CNetAddr bool IsRFC4862() const; // IPv6 autoconfig (FE80::/64) bool IsRFC6052() const; // IPv6 well-known prefix (64:FF9B::/96) bool IsRFC6145() const; // IPv6 IPv4-translated address (::FFFF:0:0:0/96) + bool IsHeNet() const; // IPv6 Hurricane Electric - https://he.net (2001:0470::/36) bool IsTor() const; bool IsLocal() const; bool IsRoutable() const; @@ -80,7 +81,19 @@ class CNetAddr unsigned int GetByte(int n) const; uint64_t GetHash() const; bool GetInAddr(struct in_addr* pipv4Addr) const; - std::vector GetGroup() const; + uint32_t GetNetClass() const; + + //! For IPv4, mapped IPv4, SIIT translated IPv4, Teredo, 6to4 tunneled addresses, return the relevant IPv4 address as a uint32. + uint32_t GetLinkedIPv4() const; + //! Whether this address has a linked IPv4 address (see GetLinkedIPv4()). + bool HasLinkedIPv4() const; + + // The AS on the BGP path to the node we use to diversify + // peers in AddrMan bucketing based on the AS infrastructure. + // The ip->AS mapping depends on how asmap is constructed. + uint32_t GetMappedAS(const std::vector &asmap) const; + + std::vector GetGroup(const std::vector &asmap) const; int GetReachabilityFrom(const CNetAddr* paddrPartner = nullptr) const; CNetAddr(const struct in6_addr& pipv6Addr, const uint32_t scope = 0); @@ -133,12 +146,11 @@ class CService : public CNetAddr public: CService(); - CService(const CNetAddr& ip, unsigned short port); - CService(const struct in_addr& ipv4Addr, unsigned short port); + CService(const CNetAddr& ip, uint16_t port); + CService(const struct in_addr& ipv4Addr, uint16_t port); CService(const struct sockaddr_in& addr); void Init(); - void SetPort(unsigned short portIn); - unsigned short GetPort() const; + uint16_t GetPort() const; bool GetSockAddr(struct sockaddr* paddr, socklen_t* addrlen) const; bool SetSockAddr(const struct sockaddr* paddr); friend bool operator==(const CService& a, const CService& b); @@ -149,7 +161,7 @@ class CService : public CNetAddr std::string ToStringPort() const; std::string ToStringIPPort() const; - CService(const struct in6_addr& ipv6Addr, unsigned short port); + CService(const struct in6_addr& ipv6Addr, uint16_t port); CService(const struct sockaddr_in6& addr); SERIALIZE_METHODS(CService, obj) { READWRITE(obj.ip, Using>(obj.port)); } diff --git a/src/netbase.cpp b/src/netbase.cpp index cb3d27543ba3..73f7b9185200 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -14,6 +14,7 @@ #include "utilstrencodings.h" #include +#include #ifndef WIN32 #include @@ -40,7 +41,7 @@ enum Network ParseNetwork(std::string net) Downcase(net); if (net == "ipv4") return NET_IPV4; if (net == "ipv6") return NET_IPV6; - if (net == "tor" || net == "onion") return NET_TOR; + if (net == "tor" || net == "onion") return NET_ONION; return NET_UNROUTABLE; } @@ -51,7 +52,7 @@ std::string GetNetworkName(enum Network net) return "ipv4"; case NET_IPV6: return "ipv6"; - case NET_TOR: + case NET_ONION: return "onion"; default: return ""; @@ -613,11 +614,13 @@ static bool ConnectThroughProxy(const proxyType &proxy, const std::string strDes ProxyCredentials random_auth; static std::atomic_int counter; random_auth.username = random_auth.password = strprintf("%i", counter++); - if (!Socks5(strDest, (unsigned short)port, &random_auth, hSocket)) + if (!Socks5(strDest, (uint16_t)port, &random_auth, hSocket)) { return false; + } } else { - if (!Socks5(strDest, (unsigned short)port, 0, hSocket)) + if (!Socks5(strDest, (uint16_t)port, 0, hSocket)) { return false; + } } hSocketRet = hSocket; diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h index 6c72a28191a8..5ac4f0441f6e 100644 --- a/src/qt/optionsmodel.h +++ b/src/qt/optionsmodel.h @@ -7,6 +7,7 @@ #define BITCOIN_QT_OPTIONSMODEL_H #include "amount.h" +#include #include #include diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index 8a7ad0d687b5..8b4ebd7926eb 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -76,6 +76,8 @@ UniValue getpeerinfo(const JSONRPCRequest& request) " \"id\": n, (numeric) Peer index\n" " \"addr\":\"host:port\", (string) The ip address and port of the peer\n" " \"addrlocal\":\"ip:port\", (string) local address\n" + " \"mapped_as\":\"mapped_as\", (string) The AS in the BGP route to the peer used for diversifying\n" + "peer selection (only available if the asmap config flag is set)\n" " \"services\":\"xxxxxxxxxxxxxxxx\", (string) The services offered\n" " \"lastsend\": ttt, (numeric) The time in seconds since epoch (Jan 1 1970 GMT) of the last send\n" " \"lastrecv\": ttt, (numeric) The time in seconds since epoch (Jan 1 1970 GMT) of the last receive\n" @@ -127,6 +129,9 @@ UniValue getpeerinfo(const JSONRPCRequest& request) obj.pushKV("addr", stats.addrName); if (!(stats.addrLocal.empty())) obj.pushKV("addrlocal", stats.addrLocal); + if (stats.m_mapped_as != 0) { + obj.pushKV("mapped_as", uint64_t(stats.m_mapped_as)); + } obj.pushKV("services", strprintf("%016x", stats.nServices)); obj.pushKV("lastsend", stats.nLastSend); obj.pushKV("lastrecv", stats.nLastRecv); diff --git a/src/serialize.h b/src/serialize.h index c9bad95cc243..9922532849df 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -11,13 +11,13 @@ #include #include #include +#include #include #include #include #include #include #include -#include #include #include #include @@ -308,14 +308,10 @@ inline void Unserialize(Stream& s, SporkId& sporkID) */ inline unsigned int GetSizeOfCompactSize(uint64_t nSize) { - if (nSize < 253) - return sizeof(unsigned char); - else if (nSize <= std::numeric_limits::max()) - return sizeof(unsigned char) + sizeof(unsigned short); - else if (nSize <= std::numeric_limits::max()) - return sizeof(unsigned char) + sizeof(unsigned int); - else - return sizeof(unsigned char) + sizeof(uint64_t); + if (nSize < 253) return sizeof(unsigned char); + else if (nSize <= std::numeric_limits::max()) return sizeof(unsigned char) + sizeof(uint16_t); + else if (nSize <= std::numeric_limits::max()) return sizeof(unsigned char) + sizeof(unsigned int); + else return sizeof(unsigned char) + sizeof(uint64_t); } inline void WriteCompactSize(CSizeComputer& os, uint64_t nSize); @@ -325,7 +321,7 @@ void WriteCompactSize(Stream& os, uint64_t nSize) { if (nSize < 253) { ser_writedata8(os, nSize); - } else if (nSize <= std::numeric_limits::max()) { + } else if (nSize <= std::numeric_limits::max()) { ser_writedata8(os, 253); ser_writedata16(os, nSize); } else if (nSize <= std::numeric_limits::max()) { diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 850a1a0811c4..cc89fd749bbb 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -28,7 +28,7 @@ function(GenerateHeaders) foreach(file IN LISTS ARGN) get_filename_component(VARNAME ${file} NAME_WE) set(outFile ${file}.h) - set(runCmd ${CMAKE_SOURCE_DIR}/contrib/devtools/gen-json-headers.sh) + set(runCmd ${CMAKE_SOURCE_DIR}/contrib/devtools/hexdump_util.sh) add_custom_command( OUTPUT ${outFile} COMMAND ${CMAKE_COMMAND} -E echo "namespace json_tests{" > ${outFile} @@ -38,7 +38,6 @@ function(GenerateHeaders) DEPENDS ${file} COMMENT "Generating ${file}.h" VERBATIM - ) list(APPEND OutputFileList ${outFile}) endforeach() @@ -52,6 +51,36 @@ endfunction() GenerateHeaders(${JSON_TEST_FILES}) +set(RAW_TEST_FILES ${CMAKE_CURRENT_SOURCE_DIR}/data/asmap.raw) + +# Generate raw files +function(GenerateRaws) + set(fileList "") + foreach(file IN LISTS ARGN) + get_filename_component(filename ${file} NAME_WE) + set(outFile ${file}.h) + set(runCmd ${CMAKE_SOURCE_DIR}/contrib/devtools/hexdump_util.sh) + add_custom_command( + OUTPUT ${outFile} + COMMAND ${CMAKE_COMMAND} -E echo "static unsigned const char ${filename}_raw[] = {" > ${outFile} + COMMAND ${runCmd} ${file} ${outFile} + COMMAND ${CMAKE_COMMAND} -E echo "};" >> ${outFile} + DEPENDS ${file} + COMMENT "Generating raw ${file}.h" + VERBATIM + ) + list(APPEND fileList ${outFile}) + endforeach() + install(FILES ${fileList} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/data) + add_custom_target( + genRaws ALL + DEPENDS ${fileList} + COMMENT "Processing raw files..." + ) +endfunction() + +GenerateRaws(${RAW_TEST_FILES}) + set(BITCOIN_TEST_SUITE ${CMAKE_CURRENT_SOURCE_DIR}/test_pivx.h ${CMAKE_CURRENT_SOURCE_DIR}/test_pivx.cpp @@ -148,7 +177,7 @@ set(BITCOIN_TESTS set(test_test_pivx_SOURCES ${BITCOIN_TEST_SUITE} ${BITCOIN_TESTS} ${JSON_TEST_FILES}) add_executable(test_pivx ${test_test_pivx_SOURCES} ${BitcoinHeaders}) -add_dependencies(test_pivx genHeaders libunivalue libsecp256k1 libzcashrust leveldb crc32c bls) +add_dependencies(test_pivx genHeaders genRaws libunivalue libsecp256k1 libzcashrust leveldb crc32c bls) target_include_directories(test_pivx PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/src/leveldb ${CMAKE_SOURCE_DIR}/src/leveldb/include diff --git a/src/test/addrman_tests.cpp b/src/test/addrman_tests.cpp index e57f50927ecd..8f71c8886864 100644 --- a/src/test/addrman_tests.cpp +++ b/src/test/addrman_tests.cpp @@ -7,6 +7,8 @@ #include #include #include // for ReadLE64 +#include "util/asmap.h" +#include "test/data/asmap.raw.h" #include "hash.h" #include "netbase.h" @@ -14,12 +16,18 @@ class CAddrManTest : public CAddrMan { - uint64_t state; - +private: + bool deterministic; public: - CAddrManTest() + explicit CAddrManTest(bool makeDeterministic = true, + std::vector asmap = std::vector()) { - state = 1; + if (makeDeterministic) { + // Set addrman addr placement to be deterministic. + MakeDeterministic(); + } + deterministic = makeDeterministic; + m_asmap = asmap; } //! Ensure that bucket placement is always the same for testing purposes. @@ -29,13 +37,13 @@ class CAddrManTest : public CAddrMan insecure_rand = FastRandomContext(true); } - CAddrInfo* Find(const CNetAddr& addr, int* pnId = NULL) + CAddrInfo* Find(const CNetAddr& addr, int* pnId = nullptr) { LOCK(cs); return CAddrMan::Find(addr, pnId); } - CAddrInfo* Create(const CAddress& addr, const CNetAddr& addrSource, int* pnId = NULL) + CAddrInfo* Create(const CAddress& addr, const CNetAddr& addrSource, int* pnId = nullptr) { LOCK(cs); return CAddrMan::Create(addr, addrSource, pnId); @@ -47,6 +55,21 @@ class CAddrManTest : public CAddrMan CAddrMan::Delete(nId); } + // Used to test deserialization + std::pair GetBucketAndEntry(const CAddress& addr) + { + LOCK(cs); + int nId = mapAddr[addr]; + for (int bucket = 0; bucket < ADDRMAN_NEW_BUCKET_COUNT; ++bucket) { + for (int entry = 0; entry < ADDRMAN_BUCKET_SIZE; ++entry) { + if (nId == vvNew[bucket][entry]) { + return std::pair(bucket, entry); + } + } + } + return std::pair(-1, -1); + } + // Simulates connection failure so that we can test eviction of offline nodes void SimConnFail(CService& addr) { @@ -58,6 +81,16 @@ class CAddrManTest : public CAddrMan int64_t nLastTry = GetAdjustedTime()-61; Attempt(addr, count_failure, nLastTry); } + + void Clear() + { + CAddrMan::Clear(); + if (deterministic) { + nKey.SetNull(); + insecure_rand = FastRandomContext(true); + } + } + }; static CNetAddr ResolveIP(const char* ip) @@ -84,15 +117,24 @@ static CService ResolveService(std::string ip, int port = 0) return ResolveService(ip.c_str(), port); } +static std::vector FromBytes(const unsigned char* source, int vector_size) { + std::vector result(vector_size); + for (int byte_i = 0; byte_i < vector_size / 8; ++byte_i) { + unsigned char cur_byte = source[byte_i]; + for (int bit_i = 0; bit_i < 8; ++bit_i) { + result[byte_i * 8 + bit_i] = (cur_byte >> bit_i) & 1; + } + } + return result; +} + + BOOST_FIXTURE_TEST_SUITE(addrman_tests, BasicTestingSetup) BOOST_AUTO_TEST_CASE(addrman_simple) { CAddrManTest addrman; - // Set addrman addr placement to be deterministic. - addrman.MakeDeterministic(); - CNetAddr source = ResolveIP("252.2.2.2"); // Test 1: Does Addrman respond correctly when empty. @@ -131,9 +173,6 @@ BOOST_AUTO_TEST_CASE(addrman_ports) { CAddrManTest addrman; - // Set addrman addr placement to be deterministic. - addrman.MakeDeterministic(); - CNetAddr source = ResolveIP("252.2.2.2"); BOOST_CHECK(addrman.size() == 0); @@ -163,9 +202,6 @@ BOOST_AUTO_TEST_CASE(addrman_select) { CAddrManTest addrman; - // Set addrman addr placement to be deterministic. - addrman.MakeDeterministic(); - CNetAddr source = ResolveIP("252.2.2.2"); // Test 9: Select from new with 1 addr in new. @@ -225,9 +261,6 @@ BOOST_AUTO_TEST_CASE(addrman_new_collisions) { CAddrManTest addrman; - // Set addrman addr placement to be deterministic. - addrman.MakeDeterministic(); - CNetAddr source = ResolveIP("252.2.2.2"); BOOST_CHECK(addrman.size() == 0); @@ -254,9 +287,6 @@ BOOST_AUTO_TEST_CASE(addrman_tried_collisions) { CAddrManTest addrman; - // Set addrman addr placement to be deterministic. - addrman.MakeDeterministic(); - CNetAddr source = ResolveIP("252.2.2.2"); BOOST_CHECK(addrman.size() == 0); @@ -285,10 +315,7 @@ BOOST_AUTO_TEST_CASE(addrman_find) { CAddrManTest addrman; - // Set addrman addr placement to be deterministic. - addrman.MakeDeterministic(); - - BOOST_CHECK(addrman.size() == 0); + BOOST_CHECK_EQUAL(addrman.size(), 0); CAddress addr1 = CAddress(ResolveService("250.1.2.1", 8333), NODE_NONE); CAddress addr2 = CAddress(ResolveService("250.1.2.1", 9999), NODE_NONE); @@ -324,10 +351,7 @@ BOOST_AUTO_TEST_CASE(addrman_create) { CAddrManTest addrman; - // Set addrman addr placement to be deterministic. - addrman.MakeDeterministic(); - - BOOST_CHECK(addrman.size() == 0); + BOOST_CHECK_EQUAL(addrman.size(), 0); CAddress addr1 = CAddress(ResolveService("250.1.2.1", 8333), NODE_NONE); CNetAddr source1 = ResolveIP("250.1.2.1"); @@ -347,10 +371,7 @@ BOOST_AUTO_TEST_CASE(addrman_delete) { CAddrManTest addrman; - // Set addrman addr placement to be deterministic. - addrman.MakeDeterministic(); - - BOOST_CHECK(addrman.size() == 0); + BOOST_CHECK_EQUAL(addrman.size(), 0); CAddress addr1 = CAddress(ResolveService("250.1.2.1", 8333), NODE_NONE); CNetAddr source1 = ResolveIP("250.1.2.1"); @@ -370,10 +391,7 @@ BOOST_AUTO_TEST_CASE(addrman_getaddr) { CAddrManTest addrman; - // Set addrman addr placement to be deterministic. - addrman.MakeDeterministic(); - - // Test 22: Sanity check, GetAddr should never return anything if addrman + // Test: Sanity check, GetAddr should never return anything if addrman // is empty. BOOST_CHECK(addrman.size() == 0); std::vector vAddr1 = addrman.GetAddr(); @@ -431,13 +449,10 @@ BOOST_AUTO_TEST_CASE(addrman_getaddr) } -BOOST_AUTO_TEST_CASE(caddrinfo_get_tried_bucket) +BOOST_AUTO_TEST_CASE(caddrinfo_get_tried_bucket_legacy) { CAddrManTest addrman; - // Set addrman addr placement to be deterministic. - addrman.MakeDeterministic(); - CAddress addr1 = CAddress(ResolveService("250.1.1.1", 8333), NODE_NONE); CAddress addr2 = CAddress(ResolveService("250.1.1.1", 9999), NODE_NONE); @@ -449,52 +464,50 @@ BOOST_AUTO_TEST_CASE(caddrinfo_get_tried_bucket) uint256 nKey1 = (uint256)(CHashWriter(SER_GETHASH, 0) << 1).GetHash(); uint256 nKey2 = (uint256)(CHashWriter(SER_GETHASH, 0) << 2).GetHash(); + std::vector asmap; // use /16 - BOOST_CHECK(info1.GetTriedBucket(nKey1) == 40); + BOOST_CHECK_EQUAL(info1.GetTriedBucket(nKey1, asmap), 40); // Test 26: Make sure key actually randomizes bucket placement. A fail on // this test could be a security issue. - BOOST_CHECK(info1.GetTriedBucket(nKey1) != info1.GetTriedBucket(nKey2)); + BOOST_CHECK(info1.GetTriedBucket(nKey1, asmap) != info1.GetTriedBucket(nKey2, asmap)); // Test 27: Two addresses with same IP but different ports can map to // different buckets because they have different keys. CAddrInfo info2 = CAddrInfo(addr2, source1); BOOST_CHECK(info1.GetKey() != info2.GetKey()); - BOOST_CHECK(info1.GetTriedBucket(nKey1) != info2.GetTriedBucket(nKey1)); + BOOST_CHECK(info1.GetTriedBucket(nKey1, asmap) != info2.GetTriedBucket(nKey1, asmap)); std::set buckets; for (int i = 0; i < 255; i++) { CAddrInfo infoi = CAddrInfo( CAddress(ResolveService("250.1.1." + std::to_string(i)), NODE_NONE), ResolveIP("250.1.1." + std::to_string(i))); - int bucket = infoi.GetTriedBucket(nKey1); + int bucket = infoi.GetTriedBucket(nKey1, asmap); buckets.insert(bucket); } - // Test 28: IP addresses in the same group (\16 prefix for IPv4) should - // never get more than 8 buckets - BOOST_CHECK(buckets.size() == 8); + // Test: IP addresses in the same /16 prefix should + // never get more than 8 buckets with legacy grouping + BOOST_CHECK_EQUAL(buckets.size(), 8U); buckets.clear(); for (int j = 0; j < 255; j++) { CAddrInfo infoj = CAddrInfo( CAddress(ResolveService("250." + std::to_string(j) + ".1.1"), NODE_NONE), ResolveIP("250." + std::to_string(j) + ".1.1")); - int bucket = infoj.GetTriedBucket(nKey1); + int bucket = infoj.GetTriedBucket(nKey1, asmap); buckets.insert(bucket); } - // Test 29: IP addresses in the different groups should map to more than - // 8 buckets. - BOOST_CHECK(buckets.size() == 160); + // Test: IP addresses in the different /16 prefix should map to more than + // 8 buckets with legacy grouping + BOOST_CHECK_EQUAL(buckets.size(), 160U); } -BOOST_AUTO_TEST_CASE(caddrinfo_get_new_bucket) +BOOST_AUTO_TEST_CASE(caddrinfo_get_new_bucket_legacy) { CAddrManTest addrman; - // Set addrman addr placement to be deterministic. - addrman.MakeDeterministic(); - CAddress addr1 = CAddress(ResolveService("250.1.2.1", 8333), NODE_NONE); CAddress addr2 = CAddress(ResolveService("250.1.2.1", 9999), NODE_NONE); @@ -505,23 +518,27 @@ BOOST_AUTO_TEST_CASE(caddrinfo_get_new_bucket) uint256 nKey1 = (uint256)(CHashWriter(SER_GETHASH, 0) << 1).GetHash(); uint256 nKey2 = (uint256)(CHashWriter(SER_GETHASH, 0) << 2).GetHash(); - BOOST_CHECK(info1.GetNewBucket(nKey1) == 786); + std::vector asmap; // use /16 + + // Test: Make sure the buckets are what we expect + BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1, asmap), 786); + BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1, source1, asmap), 786); // Test 30: Make sure key actually randomizes bucket placement. A fail on // this test could be a security issue. - BOOST_CHECK(info1.GetNewBucket(nKey1) != info1.GetNewBucket(nKey2)); + BOOST_CHECK(info1.GetNewBucket(nKey1, asmap) != info1.GetNewBucket(nKey2, asmap)); // Test 31: Ports should not effect bucket placement in the addr CAddrInfo info2 = CAddrInfo(addr2, source1); BOOST_CHECK(info1.GetKey() != info2.GetKey()); - BOOST_CHECK(info1.GetNewBucket(nKey1) == info2.GetNewBucket(nKey1)); + BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1, asmap), info2.GetNewBucket(nKey1, asmap)); std::set buckets; for (int i = 0; i < 255; i++) { CAddrInfo infoi = CAddrInfo( CAddress(ResolveService("250.1.1." + std::to_string(i)), NODE_NONE), ResolveIP("250.1.1." + std::to_string(i))); - int bucket = infoi.GetNewBucket(nKey1); + int bucket = infoi.GetNewBucket(nKey1, asmap); buckets.insert(bucket); } // Test 32: IP addresses in the same group (\16 prefix for IPv4) should @@ -534,10 +551,10 @@ BOOST_AUTO_TEST_CASE(caddrinfo_get_new_bucket) ResolveService( std::to_string(250 + (j / 255)) + "." + std::to_string(j % 256) + ".1.1"), NODE_NONE), ResolveIP("251.4.1.1")); - int bucket = infoj.GetNewBucket(nKey1); + int bucket = infoj.GetNewBucket(nKey1, asmap); buckets.insert(bucket); } - // Test 33: IP addresses in the same source groups should map to no more + // Test: IP addresses in the same source groups should map to NO MORE // than 64 buckets. BOOST_CHECK(buckets.size() <= 64); @@ -546,21 +563,230 @@ BOOST_AUTO_TEST_CASE(caddrinfo_get_new_bucket) CAddrInfo infoj = CAddrInfo( CAddress(ResolveService("250.1.1.1"), NODE_NONE), ResolveIP("250." + std::to_string(p) + ".1.1")); - int bucket = infoj.GetNewBucket(nKey1); + int bucket = infoj.GetNewBucket(nKey1, asmap); buckets.insert(bucket); } - // Test 34: IP addresses in the different source groups should map to more + // Test: IP addresses in the different source groups should map to MORE // than 64 buckets. BOOST_CHECK(buckets.size() > 64); } +// The following three test cases use asmap.raw +// We use an artificial minimal mock mapping +// 250.0.0.0/8 AS1000 +// 101.1.0.0/16 AS1 +// 101.2.0.0/16 AS2 +// 101.3.0.0/16 AS3 +// 101.4.0.0/16 AS4 +// 101.5.0.0/16 AS5 +// 101.6.0.0/16 AS6 +// 101.7.0.0/16 AS7 +// 101.8.0.0/16 AS8 +BOOST_AUTO_TEST_CASE(caddrinfo_get_tried_bucket) +{ + CAddrManTest addrman; -BOOST_AUTO_TEST_CASE(addrman_selecttriedcollision) + CAddress addr1 = CAddress(ResolveService("250.1.1.1", 8333), NODE_NONE); + CAddress addr2 = CAddress(ResolveService("250.1.1.1", 9999), NODE_NONE); + + CNetAddr source1 = ResolveIP("250.1.1.1"); + + + CAddrInfo info1 = CAddrInfo(addr1, source1); + + uint256 nKey1 = (uint256)(CHashWriter(SER_GETHASH, 0) << 1).GetHash(); + uint256 nKey2 = (uint256)(CHashWriter(SER_GETHASH, 0) << 2).GetHash(); + + std::vector asmap = FromBytes(asmap_raw, sizeof(asmap_raw) * 8); + + BOOST_CHECK_EQUAL(info1.GetTriedBucket(nKey1, asmap), 236); + + // Test: Make sure key actually randomizes bucket placement. A fail on + // this test could be a security issue. + BOOST_CHECK(info1.GetTriedBucket(nKey1, asmap) != info1.GetTriedBucket(nKey2, asmap)); + + // Test: Two addresses with same IP but different ports can map to + // different buckets because they have different keys. + CAddrInfo info2 = CAddrInfo(addr2, source1); + + BOOST_CHECK(info1.GetKey() != info2.GetKey()); + BOOST_CHECK(info1.GetTriedBucket(nKey1, asmap) != info2.GetTriedBucket(nKey1, asmap)); + + std::set buckets; + for (int j = 0; j < 255; j++) { + CAddrInfo infoj = CAddrInfo( + CAddress(ResolveService("101." + std::to_string(j) + ".1.1"), NODE_NONE), + ResolveIP("101." + std::to_string(j) + ".1.1")); + int bucket = infoj.GetTriedBucket(nKey1, asmap); + buckets.insert(bucket); + } + // Test: IP addresses in the different /16 prefix MAY map to more than + // 8 buckets. + BOOST_CHECK(buckets.size() > 8); + + buckets.clear(); + for (int j = 0; j < 255; j++) { + CAddrInfo infoj = CAddrInfo( + CAddress(ResolveService("250." + std::to_string(j) + ".1.1"), NODE_NONE), + ResolveIP("250." + std::to_string(j) + ".1.1")); + int bucket = infoj.GetTriedBucket(nKey1, asmap); + buckets.insert(bucket); + } + // Test: IP addresses in the different /16 prefix MAY NOT map to more than + // 8 buckets. + BOOST_CHECK(buckets.size() == 8); +} + +BOOST_AUTO_TEST_CASE(caddrinfo_get_new_bucket) { CAddrManTest addrman; - // Set addrman addr placement to be deterministic. - addrman.MakeDeterministic(); + CAddress addr1 = CAddress(ResolveService("250.1.2.1", 8333), NODE_NONE); + CAddress addr2 = CAddress(ResolveService("250.1.2.1", 9999), NODE_NONE); + + CNetAddr source1 = ResolveIP("250.1.2.1"); + + CAddrInfo info1 = CAddrInfo(addr1, source1); + + uint256 nKey1 = (uint256)(CHashWriter(SER_GETHASH, 0) << 1).GetHash(); + uint256 nKey2 = (uint256)(CHashWriter(SER_GETHASH, 0) << 2).GetHash(); + + std::vector asmap = FromBytes(asmap_raw, sizeof(asmap_raw) * 8); + + // Test: Make sure the buckets are what we expect + BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1, asmap), 795); + BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1, source1, asmap), 795); + + // Test: Make sure key actually randomizes bucket placement. A fail on + // this test could be a security issue. + BOOST_CHECK(info1.GetNewBucket(nKey1, asmap) != info1.GetNewBucket(nKey2, asmap)); + + // Test: Ports should not affect bucket placement in the addr + CAddrInfo info2 = CAddrInfo(addr2, source1); + BOOST_CHECK(info1.GetKey() != info2.GetKey()); + BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1, asmap), info2.GetNewBucket(nKey1, asmap)); + + std::set buckets; + for (int i = 0; i < 255; i++) { + CAddrInfo infoi = CAddrInfo( + CAddress(ResolveService("250.1.1." + std::to_string(i)), NODE_NONE), + ResolveIP("250.1.1." + std::to_string(i))); + int bucket = infoi.GetNewBucket(nKey1, asmap); + buckets.insert(bucket); + } + // Test: IP addresses in the same /16 prefix + // usually map to the same bucket. + BOOST_CHECK_EQUAL(buckets.size(), 1U); + + buckets.clear(); + for (int j = 0; j < 4 * 255; j++) { + CAddrInfo infoj = CAddrInfo(CAddress( + ResolveService( + std::to_string(250 + (j / 255)) + "." + std::to_string(j % 256) + ".1.1"), NODE_NONE), + ResolveIP("251.4.1.1")); + int bucket = infoj.GetNewBucket(nKey1, asmap); + buckets.insert(bucket); + } + // Test: IP addresses in the same source /16 prefix should not map to more + // than 64 buckets. + BOOST_CHECK(buckets.size() <= 64); + + buckets.clear(); + for (int p = 0; p < 255; p++) { + CAddrInfo infoj = CAddrInfo( + CAddress(ResolveService("250.1.1.1"), NODE_NONE), + ResolveIP("101." + std::to_string(p) + ".1.1")); + int bucket = infoj.GetNewBucket(nKey1, asmap); + buckets.insert(bucket); + } + // Test: IP addresses in the different source /16 prefixes usually map to MORE + // than 1 bucket. + BOOST_CHECK(buckets.size() > 1); + + buckets.clear(); + for (int p = 0; p < 255; p++) { + CAddrInfo infoj = CAddrInfo( + CAddress(ResolveService("250.1.1.1"), NODE_NONE), + ResolveIP("250." + std::to_string(p) + ".1.1")); + int bucket = infoj.GetNewBucket(nKey1, asmap); + buckets.insert(bucket); + } + // Test: IP addresses in the different source /16 prefixes sometimes map to NO MORE + // than 1 bucket. + BOOST_CHECK(buckets.size() == 1); + +} + +BOOST_AUTO_TEST_CASE(addrman_serialization) +{ + std::vector asmap1 = FromBytes(asmap_raw, sizeof(asmap_raw) * 8); + + CAddrManTest addrman_asmap1(true, asmap1); + CAddrManTest addrman_asmap1_dup(true, asmap1); + CAddrManTest addrman_noasmap; + CDataStream stream(SER_NETWORK, PROTOCOL_VERSION); + + CAddress addr = CAddress(ResolveService("250.1.1.1"), NODE_NONE); + CNetAddr default_source; + + + addrman_asmap1.Add(addr, default_source); + + stream << addrman_asmap1; + // serizalizing/deserializing addrman with the same asmap + stream >> addrman_asmap1_dup; + + std::pair bucketAndEntry_asmap1 = addrman_asmap1.GetBucketAndEntry(addr); + std::pair bucketAndEntry_asmap1_dup = addrman_asmap1_dup.GetBucketAndEntry(addr); + BOOST_CHECK(bucketAndEntry_asmap1.second != -1); + BOOST_CHECK(bucketAndEntry_asmap1_dup.second != -1); + + BOOST_CHECK(bucketAndEntry_asmap1.first == bucketAndEntry_asmap1_dup.first); + BOOST_CHECK(bucketAndEntry_asmap1.second == bucketAndEntry_asmap1_dup.second); + + // deserializing asmaped peers.dat to non-asmaped addrman + stream << addrman_asmap1; + stream >> addrman_noasmap; + std::pair bucketAndEntry_noasmap = addrman_noasmap.GetBucketAndEntry(addr); + BOOST_CHECK(bucketAndEntry_noasmap.second != -1); + BOOST_CHECK(bucketAndEntry_asmap1.first != bucketAndEntry_noasmap.first); + BOOST_CHECK(bucketAndEntry_asmap1.second != bucketAndEntry_noasmap.second); + + // deserializing non-asmaped peers.dat to asmaped addrman + addrman_asmap1.Clear(); + addrman_noasmap.Clear(); + addrman_noasmap.Add(addr, default_source); + stream << addrman_noasmap; + stream >> addrman_asmap1; + std::pair bucketAndEntry_asmap1_deser = addrman_asmap1.GetBucketAndEntry(addr); + BOOST_CHECK(bucketAndEntry_asmap1_deser.second != -1); + BOOST_CHECK(bucketAndEntry_asmap1_deser.first != bucketAndEntry_noasmap.first); + BOOST_CHECK(bucketAndEntry_asmap1_deser.first == bucketAndEntry_asmap1_dup.first); + BOOST_CHECK(bucketAndEntry_asmap1_deser.second == bucketAndEntry_asmap1_dup.second); + + // used to map to different buckets, now maps to the same bucket. + addrman_asmap1.Clear(); + addrman_noasmap.Clear(); + CAddress addr1 = CAddress(ResolveService("250.1.1.1"), NODE_NONE); + CAddress addr2 = CAddress(ResolveService("250.2.1.1"), NODE_NONE); + addrman_noasmap.Add(addr, default_source); + addrman_noasmap.Add(addr2, default_source); + std::pair bucketAndEntry_noasmap_addr1 = addrman_noasmap.GetBucketAndEntry(addr1); + std::pair bucketAndEntry_noasmap_addr2 = addrman_noasmap.GetBucketAndEntry(addr2); + BOOST_CHECK(bucketAndEntry_noasmap_addr1.first != bucketAndEntry_noasmap_addr2.first); + BOOST_CHECK(bucketAndEntry_noasmap_addr1.second != bucketAndEntry_noasmap_addr2.second); + stream << addrman_noasmap; + stream >> addrman_asmap1; + std::pair bucketAndEntry_asmap1_deser_addr1 = addrman_asmap1.GetBucketAndEntry(addr1); + std::pair bucketAndEntry_asmap1_deser_addr2 = addrman_asmap1.GetBucketAndEntry(addr2); + BOOST_CHECK(bucketAndEntry_asmap1_deser_addr1.first == bucketAndEntry_asmap1_deser_addr2.first); + BOOST_CHECK(bucketAndEntry_asmap1_deser_addr1.second != bucketAndEntry_asmap1_deser_addr2.second); +} + + +BOOST_AUTO_TEST_CASE(addrman_selecttriedcollision) +{ + CAddrManTest addrman; BOOST_CHECK(addrman.size() == 0); @@ -594,9 +820,6 @@ BOOST_AUTO_TEST_CASE(addrman_noevict) { CAddrManTest addrman; - // Set addrman addr placement to be deterministic. - addrman.MakeDeterministic(); - // Add twenty two addresses. CNetAddr source = ResolveIP("252.2.2.2"); for (unsigned int i = 1; i < 23; i++) { @@ -653,9 +876,6 @@ BOOST_AUTO_TEST_CASE(addrman_evictionworks) { CAddrManTest addrman; - // Set addrman addr placement to be deterministic. - addrman.MakeDeterministic(); - BOOST_CHECK(addrman.size() == 0); // Empty addrman should return blank addrman info. diff --git a/src/test/data/asmap.raw b/src/test/data/asmap.raw new file mode 100644 index 000000000000..3dcf1f394089 Binary files /dev/null and b/src/test/data/asmap.raw differ diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 25ddd959c9c9..d51dea6b9d54 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -10,6 +10,8 @@ #include "serialize.h" #include "streams.h" +#include + #include "test/test_pivx.h" #include @@ -76,6 +78,18 @@ CDataStream AddrmanToStream(CAddrManSerializationMock& addrman) BOOST_FIXTURE_TEST_SUITE(net_tests, BasicTestingSetup) +BOOST_AUTO_TEST_CASE(cnode_listen_port) +{ + // test default + uint16_t port = GetListenPort(); + BOOST_CHECK(port == Params().GetDefaultPort()); + // test set port + uint16_t altPort = 12345; + BOOST_CHECK(gArgs.SoftSetArg("-port", std::to_string(altPort))); + port = GetListenPort(); + BOOST_CHECK(port == altPort); +} + BOOST_AUTO_TEST_CASE(caddrdb_read) { CAddrManUncorrupted addrmanUncorrupted; diff --git a/src/test/netbase_tests.cpp b/src/test/netbase_tests.cpp index 6c0ff70d95b3..ccd95a1253b6 100644 --- a/src/test/netbase_tests.cpp +++ b/src/test/netbase_tests.cpp @@ -42,7 +42,7 @@ BOOST_AUTO_TEST_CASE(netbase_networks) BOOST_CHECK(ResolveIP("::1").GetNetwork() == NET_UNROUTABLE); BOOST_CHECK(ResolveIP("8.8.8.8").GetNetwork() == NET_IPV4); BOOST_CHECK(ResolveIP("2001::8888").GetNetwork() == NET_IPV6); - BOOST_CHECK(ResolveIP("FD87:D87E:EB43:edb1:8e4:3588:e546:35ca").GetNetwork() == NET_TOR); + BOOST_CHECK(ResolveIP("FD87:D87E:EB43:edb1:8e4:3588:e546:35ca").GetNetwork() == NET_ONION); BOOST_CHECK(CreateInternal("foo.com").GetNetwork() == NET_INTERNAL); } @@ -305,35 +305,36 @@ BOOST_AUTO_TEST_CASE(validate_test) BOOST_AUTO_TEST_CASE(netbase_getgroup) { - BOOST_CHECK(ResolveIP("127.0.0.1").GetGroup() == std::vector({0})); // Local -> !Routable() - BOOST_CHECK(ResolveIP("257.0.0.1").GetGroup() == std::vector({0})); // !Valid -> !Routable() - BOOST_CHECK(ResolveIP("10.0.0.1").GetGroup() == std::vector({0})); // RFC1918 -> !Routable() - BOOST_CHECK(ResolveIP("169.254.1.1").GetGroup() == std::vector({0})); // RFC3927 -> !Routable() - BOOST_CHECK(ResolveIP("1.2.3.4").GetGroup() == std::vector({(unsigned char)NET_IPV4, 1, 2})); // IPv4 - BOOST_CHECK(ResolveIP("::FFFF:0:102:304").GetGroup() == std::vector({(unsigned char)NET_IPV4, 1, 2})); // RFC6145 - BOOST_CHECK(ResolveIP("64:FF9B::102:304").GetGroup() == std::vector({(unsigned char)NET_IPV4, 1, 2})); // RFC6052 - BOOST_CHECK(ResolveIP("2002:102:304:9999:9999:9999:9999:9999").GetGroup() == std::vector({(unsigned char)NET_IPV4, 1, 2})); // RFC3964 - BOOST_CHECK(ResolveIP("2001:0:9999:9999:9999:9999:FEFD:FCFB").GetGroup() == std::vector({(unsigned char)NET_IPV4, 1, 2})); // RFC4380 - BOOST_CHECK(ResolveIP("FD87:D87E:EB43:edb1:8e4:3588:e546:35ca").GetGroup() == std::vector({(unsigned char)NET_TOR, 239})); // Tor - BOOST_CHECK(ResolveIP("2001:470:abcd:9999:9999:9999:9999:9999").GetGroup() == std::vector({(unsigned char)NET_IPV6, 32, 1, 4, 112, 175})); //he.net - BOOST_CHECK(ResolveIP("2001:2001:9999:9999:9999:9999:9999:9999").GetGroup() == std::vector({(unsigned char)NET_IPV6, 32, 1, 32, 1})); //IPv6 + std::vector asmap; // use /16 + BOOST_CHECK(ResolveIP("127.0.0.1").GetGroup(asmap) == std::vector({0})); // Local -> !Routable() + BOOST_CHECK(ResolveIP("257.0.0.1").GetGroup(asmap) == std::vector({0})); // !Valid -> !Routable() + BOOST_CHECK(ResolveIP("10.0.0.1").GetGroup(asmap) == std::vector({0})); // RFC1918 -> !Routable() + BOOST_CHECK(ResolveIP("169.254.1.1").GetGroup(asmap) == std::vector({0})); // RFC3927 -> !Routable() + BOOST_CHECK(ResolveIP("1.2.3.4").GetGroup(asmap) == std::vector({(unsigned char)NET_IPV4, 1, 2})); // IPv4 + BOOST_CHECK(ResolveIP("::FFFF:0:102:304").GetGroup(asmap) == std::vector({(unsigned char)NET_IPV4, 1, 2})); // RFC6145 + BOOST_CHECK(ResolveIP("64:FF9B::102:304").GetGroup(asmap) == std::vector({(unsigned char)NET_IPV4, 1, 2})); // RFC6052 + BOOST_CHECK(ResolveIP("2002:102:304:9999:9999:9999:9999:9999").GetGroup(asmap) == std::vector({(unsigned char)NET_IPV4, 1, 2})); // RFC3964 + BOOST_CHECK(ResolveIP("2001:0:9999:9999:9999:9999:FEFD:FCFB").GetGroup(asmap) == std::vector({(unsigned char)NET_IPV4, 1, 2})); // RFC4380 + BOOST_CHECK(ResolveIP("FD87:D87E:EB43:edb1:8e4:3588:e546:35ca").GetGroup(asmap) == std::vector({(unsigned char)NET_ONION, 239})); // Tor + BOOST_CHECK(ResolveIP("2001:470:abcd:9999:9999:9999:9999:9999").GetGroup(asmap) == std::vector({(unsigned char)NET_IPV6, 32, 1, 4, 112, 175})); //he.net + BOOST_CHECK(ResolveIP("2001:2001:9999:9999:9999:9999:9999:9999").GetGroup(asmap) == std::vector({(unsigned char)NET_IPV6, 32, 1, 32, 1})); //IPv6 // baz.net sha256 hash: 12929400eb4607c4ac075f087167e75286b179c693eb059a01774b864e8fe505 std::vector internal_group = {NET_INTERNAL, 0x12, 0x92, 0x94, 0x00, 0xeb, 0x46, 0x07, 0xc4, 0xac, 0x07}; - BOOST_CHECK(CreateInternal("baz.net").GetGroup() == internal_group); + BOOST_CHECK(CreateInternal("baz.net").GetGroup(asmap) == internal_group); } BOOST_AUTO_TEST_CASE(netbase_parsenetwork) { BOOST_CHECK_EQUAL(ParseNetwork("ipv4"), NET_IPV4); BOOST_CHECK_EQUAL(ParseNetwork("ipv6"), NET_IPV6); - BOOST_CHECK_EQUAL(ParseNetwork("onion"), NET_TOR); - BOOST_CHECK_EQUAL(ParseNetwork("tor"), NET_TOR); + BOOST_CHECK_EQUAL(ParseNetwork("onion"), NET_ONION); + BOOST_CHECK_EQUAL(ParseNetwork("tor"), NET_ONION); BOOST_CHECK_EQUAL(ParseNetwork("IPv4"), NET_IPV4); BOOST_CHECK_EQUAL(ParseNetwork("IPv6"), NET_IPV6); - BOOST_CHECK_EQUAL(ParseNetwork("ONION"), NET_TOR); - BOOST_CHECK_EQUAL(ParseNetwork("TOR"), NET_TOR); + BOOST_CHECK_EQUAL(ParseNetwork("ONION"), NET_ONION); + BOOST_CHECK_EQUAL(ParseNetwork("TOR"), NET_ONION); BOOST_CHECK_EQUAL(ParseNetwork(":)"), NET_UNROUTABLE); BOOST_CHECK_EQUAL(ParseNetwork("tÖr"), NET_UNROUTABLE); diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index 1f89e6f78c3f..485057a6b0d6 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -530,8 +530,8 @@ void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply& if (gArgs.GetArg("-onion", "") == "") { CService resolved(LookupNumeric("127.0.0.1", 9050)); proxyType addrOnion = proxyType(resolved, true); - SetProxy(NET_TOR, addrOnion); - SetLimited(NET_TOR, false); + SetProxy(NET_ONION, addrOnion); + SetLimited(NET_ONION, false); } // Finally - now create the service diff --git a/src/util/asmap.cpp b/src/util/asmap.cpp new file mode 100644 index 000000000000..60bd27bf909e --- /dev/null +++ b/src/util/asmap.cpp @@ -0,0 +1,103 @@ +// Copyright (c) 2019 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 +#include +#include + +namespace { + +uint32_t DecodeBits(std::vector::const_iterator& bitpos, const std::vector::const_iterator& endpos, uint8_t minval, const std::vector &bit_sizes) +{ + uint32_t val = minval; + bool bit; + for (std::vector::const_iterator bit_sizes_it = bit_sizes.begin(); + bit_sizes_it != bit_sizes.end(); ++bit_sizes_it) { + if (bit_sizes_it + 1 != bit_sizes.end()) { + if (bitpos == endpos) break; + bit = *bitpos; + bitpos++; + } else { + bit = 0; + } + if (bit) { + val += (1 << *bit_sizes_it); + } else { + for (int b = 0; b < *bit_sizes_it; b++) { + if (bitpos == endpos) break; + bit = *bitpos; + bitpos++; + val += bit << (*bit_sizes_it - 1 - b); + } + return val; + } + } + return -1; +} + +const std::vector TYPE_BIT_SIZES{0, 0, 1}; +uint32_t DecodeType(std::vector::const_iterator& bitpos, const std::vector::const_iterator& endpos) +{ + return DecodeBits(bitpos, endpos, 0, TYPE_BIT_SIZES); +} + +const std::vector ASN_BIT_SIZES{15, 16, 17, 18, 19, 20, 21, 22, 23, 24}; +uint32_t DecodeASN(std::vector::const_iterator& bitpos, const std::vector::const_iterator& endpos) +{ + return DecodeBits(bitpos, endpos, 1, ASN_BIT_SIZES); +} + + +const std::vector MATCH_BIT_SIZES{1, 2, 3, 4, 5, 6, 7, 8}; +uint32_t DecodeMatch(std::vector::const_iterator& bitpos, const std::vector::const_iterator& endpos) +{ + return DecodeBits(bitpos, endpos, 2, MATCH_BIT_SIZES); +} + + +const std::vector JUMP_BIT_SIZES{5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30}; +uint32_t DecodeJump(std::vector::const_iterator& bitpos, const std::vector::const_iterator& endpos) +{ + return DecodeBits(bitpos, endpos, 17, JUMP_BIT_SIZES); +} + +} + +uint32_t Interpret(const std::vector &asmap, const std::vector &ip) +{ + std::vector::const_iterator pos = asmap.begin(); + const std::vector::const_iterator endpos = asmap.end(); + uint8_t bits = ip.size(); + uint32_t default_asn = 0; + uint32_t opcode, jump, match, matchlen; + while (pos != endpos) { + opcode = DecodeType(pos, endpos); + if (opcode == 0) { + return DecodeASN(pos, endpos); + } else if (opcode == 1) { + jump = DecodeJump(pos, endpos); + if (bits == 0) break; + if (ip[ip.size() - bits]) { + if (jump >= endpos - pos) break; + pos += jump; + } + bits--; + } else if (opcode == 2) { + match = DecodeMatch(pos, endpos); + matchlen = CountBits(match) - 1; + for (uint32_t bit = 0; bit < matchlen; bit++) { + if (bits == 0) break; + if ((ip[ip.size() - bits]) != ((match >> (matchlen - 1 - bit)) & 1)) { + return default_asn; + } + bits--; + } + } else if (opcode == 3) { + default_asn = DecodeASN(pos, endpos); + } else { + break; + } + } + return 0; // 0 is not a valid ASN +} diff --git a/src/util/asmap.h b/src/util/asmap.h new file mode 100644 index 000000000000..a0e14013c56a --- /dev/null +++ b/src/util/asmap.h @@ -0,0 +1,10 @@ +// Copyright (c) 2019 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_UTIL_ASMAP_H +#define BITCOIN_UTIL_ASMAP_H + +uint32_t Interpret(const std::vector &asmap, const std::vector &ip); + +#endif // BITCOIN_UTIL_ASMAP_H diff --git a/test/functional/feature_asmap.py b/test/functional/feature_asmap.py new file mode 100755 index 000000000000..30f5f482270d --- /dev/null +++ b/test/functional/feature_asmap.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# 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. +"""Test asmap config argument for ASN-based IP bucketing. + +Verify node behaviour and debug log when launching pivxd in these cases: + +1. `pivxd` with no -asmap arg, using /16 prefix for IP bucketing + +2. `pivxd -asmap=`, using the unit test skeleton asmap + +3. `pivxd -asmap=`, using the unit test skeleton asmap + +4. `pivxd -asmap/-asmap=` with no file specified, using the default asmap + +5. `pivxd -asmap` with no file specified and a missing default asmap file + +6. `pivxd -asmap` with an empty (unparsable) default asmap file + +The tests are order-independent. + +""" +import os +import shutil + +from test_framework.test_framework import PivxTestFramework + +DEFAULT_ASMAP_FILENAME = 'ip_asn.map' # defined in src/init.cpp +ASMAP = '../../src/test/data/asmap.raw' # path to unit test skeleton asmap +VERSION = 'fec61fa21a9f46f3b17bdcd660d7f4cd90b966aad3aec593c99b35f0aca15853' + +def expected_messages(filename): + return ['Opened asmap file "{}" (59 bytes) from disk'.format(filename), + 'Using asmap version {} for IP bucketing'.format(VERSION)] + +class AsmapTest(PivxTestFramework): + def set_test_params(self): + self.num_nodes = 1 + + def test_without_asmap_arg(self): + self.log.info('Test pivxd with no -asmap arg passed') + self.stop_node(0) + with self.node.assert_debug_log(['Using /16 prefix for IP bucketing']): + self.start_node(0) + + def test_asmap_with_absolute_path(self): + self.log.info('Test pivxd -asmap=') + self.stop_node(0) + filename = os.path.join(self.datadir, 'my-map-file.map') + shutil.copyfile(self.asmap_raw, filename) + with self.node.assert_debug_log(expected_messages(filename)): + self.start_node(0, ['-asmap={}'.format(filename)]) + os.remove(filename) + + def test_asmap_with_relative_path(self): + self.log.info('Test pivxd -asmap=') + self.stop_node(0) + name = 'ASN_map' + filename = os.path.join(self.datadir, name) + shutil.copyfile(self.asmap_raw, filename) + with self.node.assert_debug_log(expected_messages(filename)): + self.start_node(0, ['-asmap={}'.format(name)]) + os.remove(filename) + + def test_default_asmap(self): + shutil.copyfile(self.asmap_raw, self.default_asmap) + for arg in ['-asmap', '-asmap=']: + self.log.info('Test pivxd {} (using default map file)'.format(arg)) + self.stop_node(0) + with self.node.assert_debug_log(expected_messages(self.default_asmap)): + self.start_node(0, [arg]) + os.remove(self.default_asmap) + + def test_default_asmap_with_missing_file(self): + self.log.info('Test pivxd -asmap with missing default map file') + self.stop_node(0) + msg = "Error: Could not find asmap file \"{}\"".format(self.default_asmap) + self.assert_start_raises_init_error(0, extra_args=['-asmap'], expected_msg=msg) + + def test_empty_asmap(self): + self.log.info('Test pivxd -asmap with empty map file') + self.stop_node(0) + with open(self.default_asmap, "w", encoding="utf-8") as f: + f.write("") + msg = "Error: Could not parse asmap file \"{}\"".format(self.default_asmap) + self.assert_start_raises_init_error(0, extra_args=['-asmap'], expected_msg=msg) + os.remove(self.default_asmap) + + def run_test(self): + self.node = self.nodes[0] + self.datadir = os.path.join(self.node.datadir, 'regtest') + self.default_asmap = os.path.join(self.datadir, DEFAULT_ASMAP_FILENAME) + self.asmap_raw = os.path.join(os.path.dirname(os.path.realpath(__file__)), ASMAP) + + self.test_without_asmap_arg() + self.test_asmap_with_absolute_path() + self.test_asmap_with_relative_path() + self.test_default_asmap() + self.test_default_asmap_with_missing_file() + self.test_empty_asmap() + + +if __name__ == '__main__': + AsmapTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 29d73196005a..d72f92583cfb 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -121,6 +121,7 @@ 'rpc_signrawtransaction.py', # ~ 50 sec 'rpc_decodescript.py', # ~ 50 sec 'rpc_blockchain.py', # ~ 50 sec + 'feature_asmap.py', 'wallet_disable.py', # ~ 50 sec 'wallet_autocombine.py', # ~ 49 sec 'mining_v5_upgrade.py', # ~ 48 sec