Skip to content

Commit

Permalink
Merge #29007: test: create deterministic addrman in the functional tests
Browse files Browse the repository at this point in the history
2cc8ca1 [test] Use deterministic addrman in addrman info tests (stratospher)
a897866 [test] Restart a node with empty addrman (stratospher)
71c1991 [test] Use deterministic addrman in addpeeraddress test (stratospher)
7b868e6 Revert "test: avoid non-determinism in asmap-addrman test" (stratospher)
69e091f [init] Create deterministic addrman in tests using -test=addrman (stratospher)
be25ac3 [init] Remove -addrmantest command line arg (stratospher)
802e6e1 [init] Add new command line arg for use only in functional tests (stratospher)

Pull request description:

  An address is placed in a `[bucket,position]` in the addrman table (new table or tried table) using the `addpeeraddress` RPC. This `[bucket,position]` is calculated using `nKey`(and other metrics) for the addrman which is chosen randomly during every run.

  Supposing there are 2 addresses to be placed in an addrman table. During every test run, a different `[bucket,position]` would be calculated for each address.These calculated `[bucket,position]` could even be the same for the 2 addresses in some test runs and result in collisions in the addrman. We wouldn't be able to predict when the collisions are going to happen because we can't predict the `nKey` value which is chosen at random. This can cause flaky tests.

  Because of these non deterministic collisions, we are limited in what we can do to test addrman functionality. Currently in our tests don't add a second address to prevent these collisions from happening - we only keep 1 address in the new table and 1 address in the tried table. See #26988 (comment), #23084, [#22831(comment)](https://github.com/bitcoin/bitcoin/pull/22831/files#r708302639).

  This PR lets us create a deterministic addrman with fixed `nKey` so that we can know the `[bucket,position]` collisions beforehand, safely add more addresses in an addrman table and write more extensive tests.

ACKs for top commit:
  amitiuttarwar:
    ACK 2cc8ca1
  achow101:
    ACK 2cc8ca1
  0xB10C:
    ACK 2cc8ca1
  mzumsande:
    Code Review ACK 2cc8ca1

Tree-SHA512: 8acd9bdfe7de1eb44d22373bf13533d8ecf602df966fdd5b8b78afcd8cc35a286c95d2712f67a89473a0d68dded7d38f5599f6e4bf95a6589475444545bfb189
  • Loading branch information
achow101 committed Mar 11, 2024
2 parents 6dda050 + 2cc8ca1 commit a945f09
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 115 deletions.
8 changes: 5 additions & 3 deletions src/addrdb.cpp
Expand Up @@ -189,7 +189,9 @@ void ReadFromStream(AddrMan& addr, DataStream& ssPeers)
util::Result<std::unique_ptr<AddrMan>> LoadAddrman(const NetGroupManager& netgroupman, const ArgsManager& args)
{
auto check_addrman = std::clamp<int32_t>(args.GetIntArg("-checkaddrman", DEFAULT_ADDRMAN_CONSISTENCY_CHECKS), 0, 1000000);
auto addrman{std::make_unique<AddrMan>(netgroupman, /*deterministic=*/false, /*consistency_check_ratio=*/check_addrman)};
bool deterministic = HasTestOption(args, "addrman"); // use a deterministic addrman only for tests

auto addrman{std::make_unique<AddrMan>(netgroupman, /*deterministic=*/deterministic, /*consistency_check_ratio=*/check_addrman)};

const auto start{SteadyClock::now()};
const auto path_addr{args.GetDataDirNet() / "peers.dat"};
Expand All @@ -198,15 +200,15 @@ util::Result<std::unique_ptr<AddrMan>> LoadAddrman(const NetGroupManager& netgro
LogPrintf("Loaded %i addresses from peers.dat %dms\n", addrman->Size(), Ticks<std::chrono::milliseconds>(SteadyClock::now() - start));
} catch (const DbNotFoundError&) {
// Addrman can be in an inconsistent state after failure, reset it
addrman = std::make_unique<AddrMan>(netgroupman, /*deterministic=*/false, /*consistency_check_ratio=*/check_addrman);
addrman = std::make_unique<AddrMan>(netgroupman, /*deterministic=*/deterministic, /*consistency_check_ratio=*/check_addrman);
LogPrintf("Creating peers.dat because the file was not found (%s)\n", fs::quoted(fs::PathToString(path_addr)));
DumpPeerAddresses(args, *addrman);
} catch (const InvalidAddrManVersionError&) {
if (!RenameOver(path_addr, (fs::path)path_addr + ".bak")) {
return util::Error{strprintf(_("Failed to rename invalid peers.dat file. Please move or delete it and try again."))};
}
// Addrman can be in an inconsistent state after failure, reset it
addrman = std::make_unique<AddrMan>(netgroupman, /*deterministic=*/false, /*consistency_check_ratio=*/check_addrman);
addrman = std::make_unique<AddrMan>(netgroupman, /*deterministic=*/deterministic, /*consistency_check_ratio=*/check_addrman);
LogPrintf("Creating new peers.dat because the file version was not compatible (%s). Original backed up to peers.dat.bak\n", fs::quoted(fs::PathToString(path_addr)));
DumpPeerAddresses(args, *addrman);
} catch (const std::exception& e) {
Expand Down
12 changes: 12 additions & 0 deletions src/common/args.cpp
Expand Up @@ -682,6 +682,18 @@ std::string HelpMessageOpt(const std::string &option, const std::string &message
std::string("\n\n");
}

const std::vector<std::string> TEST_OPTIONS_DOC{
"addrman (use deterministic addrman)",
};

bool HasTestOption(const ArgsManager& args, const std::string& test_option)
{
const auto options = args.GetArgs("-test");
return std::any_of(options.begin(), options.end(), [test_option](const auto& option) {
return option == test_option;
});
}

fs::path GetDefaultDataDir()
{
// Windows: C:\Users\Username\AppData\Roaming\Bitcoin
Expand Down
5 changes: 5 additions & 0 deletions src/common/args.h
Expand Up @@ -447,6 +447,11 @@ bool HelpRequested(const ArgsManager& args);
/** Add help options to the args manager */
void SetupHelpOptions(ArgsManager& args);

extern const std::vector<std::string> TEST_OPTIONS_DOC;

/** Checks if a particular test option is present in -test command-line arg options */
bool HasTestOption(const ArgsManager& args, const std::string& test_option);

/**
* Format a string to be used as group of options in help messages
*
Expand Down
18 changes: 17 additions & 1 deletion src/init.cpp
Expand Up @@ -614,7 +614,7 @@ void SetupServerArgs(ArgsManager& argsman)
argsman.AddArg("-limitancestorsize=<n>", strprintf("Do not accept transactions whose size with all in-mempool ancestors exceeds <n> kilobytes (default: %u)", DEFAULT_ANCESTOR_SIZE_LIMIT_KVB), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-limitdescendantcount=<n>", strprintf("Do not accept transactions if any ancestor would have <n> or more in-mempool descendants (default: %u)", DEFAULT_DESCENDANT_LIMIT), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-limitdescendantsize=<n>", strprintf("Do not accept transactions if any ancestor would have more than <n> kilobytes of in-mempool descendants (default: %u).", DEFAULT_DESCENDANT_SIZE_LIMIT_KVB), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-addrmantest", "Allows to test address relay on localhost", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-test=<option>", "Pass a test-only option. Options include : " + Join(TEST_OPTIONS_DOC, ", ") + ".", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-capturemessages", "Capture all P2P messages to disk", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-mocktime=<n>", "Replace actual time with " + UNIX_EPOCH_TIME + " (default: 0)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-maxsigcachesize=<n>", strprintf("Limit sum of signature cache and script execution cache sizes to <n> MiB (default: %u)", DEFAULT_MAX_SIG_CACHE_BYTES >> 20), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
Expand Down Expand Up @@ -1028,6 +1028,22 @@ bool AppInitParameterInteraction(const ArgsManager& args)
if (args.GetBoolArg("-peerbloomfilters", DEFAULT_PEERBLOOMFILTERS))
nLocalServices = ServiceFlags(nLocalServices | NODE_BLOOM);

if (args.IsArgSet("-test")) {
if (chainparams.GetChainType() != ChainType::REGTEST) {
return InitError(Untranslated("-test=<option> should only be used in functional tests"));
}
const std::vector<std::string> options = args.GetArgs("-test");
for (const std::string& option : options) {
auto it = std::find_if(TEST_OPTIONS_DOC.begin(), TEST_OPTIONS_DOC.end(), [&option](const std::string& doc_option) {
size_t pos = doc_option.find(" (");
return (pos != std::string::npos) && (doc_option.substr(0, pos) == option);
});
if (it == TEST_OPTIONS_DOC.end()) {
InitWarning(strprintf(_("Unrecognised option \"%s\" provided in -test=<option>."), option));
}
}
}

// Also report errors from parsing before daemonization
{
kernel::Notifications notifications{};
Expand Down
7 changes: 1 addition & 6 deletions src/net.cpp
Expand Up @@ -238,10 +238,6 @@ static int GetnScore(const CService& addr)
std::optional<CService> GetLocalAddrForPeer(CNode& node)
{
CService addrLocal{GetLocalAddress(node)};
if (gArgs.GetBoolArg("-addrmantest", false)) {
// use IPv4 loopback during addrmantest
addrLocal = CService(LookupNumeric("127.0.0.1", GetListenPort()));
}
// If discovery is enabled, sometimes give our peer the address it
// tells us that it sees us as in case it has a better idea of our
// address than we do.
Expand All @@ -261,8 +257,7 @@ std::optional<CService> GetLocalAddrForPeer(CNode& node)
addrLocal.SetIP(node.GetAddrLocal());
}
}
if (addrLocal.IsRoutable() || gArgs.GetBoolArg("-addrmantest", false))
{
if (addrLocal.IsRoutable()) {
LogPrint(BCLog::NET, "Advertising address %s to peer=%d\n", addrLocal.ToStringAddrPort(), node.GetId());
return addrLocal;
}
Expand Down
7 changes: 1 addition & 6 deletions test/functional/feature_addrman.py
Expand Up @@ -156,12 +156,7 @@ def run_test(self):
)

self.log.info("Check that missing addrman is recreated")
self.stop_node(0)
os.remove(peers_dat)
with self.nodes[0].assert_debug_log([
f'Creating peers.dat because the file was not found ("{peers_dat}")',
]):
self.start_node(0)
self.restart_node(0, clear_addrman=True)
assert_equal(self.nodes[0].getnodeaddresses(), [])


Expand Down
12 changes: 6 additions & 6 deletions test/functional/feature_asmap.py
Expand Up @@ -42,8 +42,8 @@ def set_test_params(self):
self.extra_args = [["-checkaddrman=1"]] # Do addrman checks on all operations.

def fill_addrman(self, node_id):
"""Add 1 tried address to the addrman, followed by 1 new address."""
for addr, tried in [[0, True], [1, False]]:
"""Add 2 tried addresses to the addrman, followed by 2 new addresses."""
for addr, tried in [[0, True], [1, True], [2, False], [3, False]]:
self.nodes[node_id].addpeeraddress(address=f"101.{addr}.0.0", tried=tried, port=8333)

def test_without_asmap_arg(self):
Expand Down Expand Up @@ -84,12 +84,12 @@ def test_asmap_interaction_with_addrman_containing_entries(self):
self.log.info("Test bitcoind -asmap restart with addrman containing new and tried entries")
self.stop_node(0)
shutil.copyfile(self.asmap_raw, self.default_asmap)
self.start_node(0, ["-asmap", "-checkaddrman=1"])
self.start_node(0, ["-asmap", "-checkaddrman=1", "-test=addrman"])
self.fill_addrman(node_id=0)
self.restart_node(0, ["-asmap", "-checkaddrman=1"])
self.restart_node(0, ["-asmap", "-checkaddrman=1", "-test=addrman"])
with self.node.assert_debug_log(
expected_msgs=[
"CheckAddrman: new 1, tried 1, total 2 started",
"CheckAddrman: new 2, tried 2, total 4 started",
"CheckAddrman: completed",
]
):
Expand All @@ -114,7 +114,7 @@ def test_empty_asmap(self):
def test_asmap_health_check(self):
self.log.info('Test bitcoind -asmap logs ASMap Health Check with basic stats')
shutil.copyfile(self.asmap_raw, self.default_asmap)
msg = "ASMap Health Check: 2 clearnet peers are mapped to 1 ASNs with 0 peers being unmapped"
msg = "ASMap Health Check: 4 clearnet peers are mapped to 3 ASNs with 0 peers being unmapped"
with self.node.assert_debug_log(expected_msgs=[msg]):
self.start_node(0, extra_args=['-asmap'])
os.remove(self.default_asmap)
Expand Down
13 changes: 1 addition & 12 deletions test/functional/p2p_node_network_limited.py
Expand Up @@ -15,7 +15,6 @@
NODE_P2P_V2,
NODE_WITNESS,
msg_getdata,
msg_verack,
)
from test_framework.p2p import P2PInterface
from test_framework.test_framework import BitcoinTestFramework
Expand Down Expand Up @@ -47,7 +46,7 @@ class NodeNetworkLimitedTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 3
self.extra_args = [['-prune=550', '-addrmantest'], [], []]
self.extra_args = [['-prune=550'], [], []]

def disconnect_all(self):
self.disconnect_nodes(0, 1)
Expand Down Expand Up @@ -139,16 +138,6 @@ def run_test(self):
self.log.info("Requesting block at height 2 (tip-289) must fail (ignored).")
node.send_getdata_for_block(blocks[0]) # first block outside of the 288+2 limit
node.wait_for_disconnect(5)

self.log.info("Check local address relay, do a fresh connection.")
self.nodes[0].disconnect_p2ps()
node1 = self.nodes[0].add_p2p_connection(P2PIgnoreInv())
node1.send_message(msg_verack())

node1.wait_for_addr()
#must relay address with NODE_NETWORK_LIMITED
assert_equal(node1.firstAddrnServices, expected_services)

self.nodes[0].disconnect_p2ps()

# connect unsynced node 2 with pruned NODE_NETWORK_LIMITED peer
Expand Down

0 comments on commit a945f09

Please sign in to comment.