Skip to content

Commit 01bd9d7

Browse files
committed
Merge bitcoin/bitcoin#28523: rpc: add hidden getrawaddrman RPC to list addrman table entries
352d5eb test: getrawaddrman RPC (0xb10c) da384a2 rpc: getrawaddrman for addrman entries (0xb10c) Pull request description: Inspired by `getaddrmaninfo` (#27511), this adds a hidden/test-only `getrawaddrman` RPC. The RPC returns information on all addresses in the address manager new and tried tables. Addrman table contents can be used in tests and during development. The RPC result encodes the `bucket` and `position`, the internal location of addresses in the tables, in the address object's string key. This allows users to choose to consume or to ignore the location information. If the internals of the address manager implementation change, the location encoding might change too. ``` getrawaddrman EXPERIMENTAL warning: this call may be changed in future releases. Returns information on all address manager entries for the new and tried tables. Result: { (json object) "table" : { (json object) buckets with addresses in the address manager table ( new, tried ) "bucket/position" : { (json object) the location in the address manager table (<bucket>/<position>) "address" : "str", (string) The address of the node "port" : n, (numeric) The port number of the node "network" : "str", (string) The network (ipv4, ipv6, onion, i2p, cjdns) of the address "services" : n, (numeric) The services offered by the node "time" : xxx, (numeric) The UNIX epoch time when the node was last seen "source" : "str", (string) The address that relayed the address to us "source_network" : "str" (string) The network (ipv4, ipv6, onion, i2p, cjdns) of the source address }, ... }, ... } Examples: > bitcoin-cli getrawaddrman > curl --user myusername --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "getrawaddrman", "params": []}' -H 'content-type: text/plain;' http://127.0.0.1:8332/ ``` ACKs for top commit: willcl-ark: reACK 352d5eb amitiuttarwar: reACK 352d5eb stratospher: reACK 352d5eb. achow101: ACK 352d5eb Tree-SHA512: cc462666b5c709617c66b0e3e9a17c4c81e9e295f91bdd9572492d1cb6466fc9b6d48ee805ebe82f9f16010798370effe5c8f4db15065b8c7c0d8637675d615e
2 parents 5a4eb56 + 352d5eb commit 01bd9d7

File tree

6 files changed

+239
-1
lines changed

6 files changed

+239
-1
lines changed

src/addrman.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,30 @@ std::vector<CAddress> AddrManImpl::GetAddr_(size_t max_addresses, size_t max_pct
838838
return addresses;
839839
}
840840

841+
std::vector<std::pair<AddrInfo, AddressPosition>> AddrManImpl::GetEntries_(bool from_tried) const
842+
{
843+
AssertLockHeld(cs);
844+
845+
const int bucket_count = from_tried ? ADDRMAN_TRIED_BUCKET_COUNT : ADDRMAN_NEW_BUCKET_COUNT;
846+
std::vector<std::pair<AddrInfo, AddressPosition>> infos;
847+
for (int bucket = 0; bucket < bucket_count; ++bucket) {
848+
for (int position = 0; position < ADDRMAN_BUCKET_SIZE; ++position) {
849+
int id = GetEntry(from_tried, bucket, position);
850+
if (id >= 0) {
851+
AddrInfo info = mapInfo.at(id);
852+
AddressPosition location = AddressPosition(
853+
from_tried,
854+
/*multiplicity_in=*/from_tried ? 1 : info.nRefCount,
855+
bucket,
856+
position);
857+
infos.push_back(std::make_pair(info, location));
858+
}
859+
}
860+
}
861+
862+
return infos;
863+
}
864+
841865
void AddrManImpl::Connected_(const CService& addr, NodeSeconds time)
842866
{
843867
AssertLockHeld(cs);
@@ -1199,6 +1223,15 @@ std::vector<CAddress> AddrManImpl::GetAddr(size_t max_addresses, size_t max_pct,
11991223
return addresses;
12001224
}
12011225

1226+
std::vector<std::pair<AddrInfo, AddressPosition>> AddrManImpl::GetEntries(bool from_tried) const
1227+
{
1228+
LOCK(cs);
1229+
Check();
1230+
auto addrInfos = GetEntries_(from_tried);
1231+
Check();
1232+
return addrInfos;
1233+
}
1234+
12021235
void AddrManImpl::Connected(const CService& addr, NodeSeconds time)
12031236
{
12041237
LOCK(cs);
@@ -1289,6 +1322,11 @@ std::vector<CAddress> AddrMan::GetAddr(size_t max_addresses, size_t max_pct, std
12891322
return m_impl->GetAddr(max_addresses, max_pct, network);
12901323
}
12911324

1325+
std::vector<std::pair<AddrInfo, AddressPosition>> AddrMan::GetEntries(bool use_tried) const
1326+
{
1327+
return m_impl->GetEntries(use_tried);
1328+
}
1329+
12921330
void AddrMan::Connected(const CService& addr, NodeSeconds time)
12931331
{
12941332
m_impl->Connected(addr, time);

src/addrman.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ class InvalidAddrManVersionError : public std::ios_base::failure
2525
};
2626

2727
class AddrManImpl;
28+
class AddrInfo;
2829

2930
/** Default for -checkaddrman */
3031
static constexpr int32_t DEFAULT_ADDRMAN_CONSISTENCY_CHECKS{0};
3132

32-
/** Test-only struct, capturing info about an address in AddrMan */
33+
/** Location information for an address in AddrMan */
3334
struct AddressPosition {
3435
// Whether the address is in the new or tried table
3536
const bool tried;
@@ -168,6 +169,17 @@ class AddrMan
168169
*/
169170
std::vector<CAddress> GetAddr(size_t max_addresses, size_t max_pct, std::optional<Network> network) const;
170171

172+
/**
173+
* Returns an information-location pair for all addresses in the selected addrman table.
174+
* If an address appears multiple times in the new table, an information-location pair
175+
* is returned for each occurence. Addresses only ever appear once in the tried table.
176+
*
177+
* @param[in] from_tried Selects which table to return entries from.
178+
*
179+
* @return A vector consisting of pairs of AddrInfo and AddressPosition.
180+
*/
181+
std::vector<std::pair<AddrInfo, AddressPosition>> GetEntries(bool from_tried) const;
182+
171183
/** We have successfully connected to this peer. Calling this function
172184
* updates the CAddress's nTime, which is used in our IsTerrible()
173185
* decisions and gossiped to peers. Callers should be careful that updating

src/addrman_impl.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ class AddrManImpl
132132
std::vector<CAddress> GetAddr(size_t max_addresses, size_t max_pct, std::optional<Network> network) const
133133
EXCLUSIVE_LOCKS_REQUIRED(!cs);
134134

135+
std::vector<std::pair<AddrInfo, AddressPosition>> GetEntries(bool from_tried) const
136+
EXCLUSIVE_LOCKS_REQUIRED(!cs);
137+
135138
void Connected(const CService& addr, NodeSeconds time)
136139
EXCLUSIVE_LOCKS_REQUIRED(!cs);
137140

@@ -260,6 +263,8 @@ class AddrManImpl
260263

261264
std::vector<CAddress> GetAddr_(size_t max_addresses, size_t max_pct, std::optional<Network> network) const EXCLUSIVE_LOCKS_REQUIRED(cs);
262265

266+
std::vector<std::pair<AddrInfo, AddressPosition>> GetEntries_(bool from_tried) const EXCLUSIVE_LOCKS_REQUIRED(cs);
267+
263268
void Connected_(const CService& addr, NodeSeconds time) EXCLUSIVE_LOCKS_REQUIRED(cs);
264269

265270
void SetServices_(const CService& addr, ServiceFlags nServices) EXCLUSIVE_LOCKS_REQUIRED(cs);

src/rpc/net.cpp

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <rpc/server.h>
66

77
#include <addrman.h>
8+
#include <addrman_impl.h>
89
#include <banman.h>
910
#include <chainparams.h>
1011
#include <clientversion.h>
@@ -1079,6 +1080,74 @@ static RPCHelpMan getaddrmaninfo()
10791080
};
10801081
}
10811082

1083+
UniValue AddrmanEntryToJSON(const AddrInfo& info)
1084+
{
1085+
UniValue ret(UniValue::VOBJ);
1086+
ret.pushKV("address", info.ToStringAddr());
1087+
ret.pushKV("port", info.GetPort());
1088+
ret.pushKV("services", (uint64_t)info.nServices);
1089+
ret.pushKV("time", int64_t{TicksSinceEpoch<std::chrono::seconds>(info.nTime)});
1090+
ret.pushKV("network", GetNetworkName(info.GetNetClass()));
1091+
ret.pushKV("source", info.source.ToStringAddr());
1092+
ret.pushKV("source_network", GetNetworkName(info.source.GetNetClass()));
1093+
return ret;
1094+
}
1095+
1096+
UniValue AddrmanTableToJSON(const std::vector<std::pair<AddrInfo, AddressPosition>>& tableInfos)
1097+
{
1098+
UniValue table(UniValue::VOBJ);
1099+
for (const auto& e : tableInfos) {
1100+
AddrInfo info = e.first;
1101+
AddressPosition location = e.second;
1102+
std::ostringstream key;
1103+
key << location.bucket << "/" << location.position;
1104+
// Address manager tables have unique entries so there is no advantage
1105+
// in using UniValue::pushKV, which checks if the key already exists
1106+
// in O(N). UniValue::pushKVEnd is used instead which currently is O(1).
1107+
table.pushKVEnd(key.str(), AddrmanEntryToJSON(info));
1108+
}
1109+
return table;
1110+
}
1111+
1112+
static RPCHelpMan getrawaddrman()
1113+
{
1114+
return RPCHelpMan{"getrawaddrman",
1115+
"EXPERIMENTAL warning: this call may be changed in future releases.\n"
1116+
"\nReturns information on all address manager entries for the new and tried tables.\n",
1117+
{},
1118+
RPCResult{
1119+
RPCResult::Type::OBJ_DYN, "", "", {
1120+
{RPCResult::Type::OBJ_DYN, "table", "buckets with addresses in the address manager table ( new, tried )", {
1121+
{RPCResult::Type::OBJ, "bucket/position", "the location in the address manager table (<bucket>/<position>)", {
1122+
{RPCResult::Type::STR, "address", "The address of the node"},
1123+
{RPCResult::Type::NUM, "port", "The port number of the node"},
1124+
{RPCResult::Type::STR, "network", "The network (" + Join(GetNetworkNames(), ", ") + ") of the address"},
1125+
{RPCResult::Type::NUM, "services", "The services offered by the node"},
1126+
{RPCResult::Type::NUM_TIME, "time", "The " + UNIX_EPOCH_TIME + " when the node was last seen"},
1127+
{RPCResult::Type::STR, "source", "The address that relayed the address to us"},
1128+
{RPCResult::Type::STR, "source_network", "The network (" + Join(GetNetworkNames(), ", ") + ") of the source address"},
1129+
}}
1130+
}}
1131+
}
1132+
},
1133+
RPCExamples{
1134+
HelpExampleCli("getrawaddrman", "")
1135+
+ HelpExampleRpc("getrawaddrman", "")
1136+
},
1137+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue {
1138+
NodeContext& node = EnsureAnyNodeContext(request.context);
1139+
if (!node.addrman) {
1140+
throw JSONRPCError(RPC_CLIENT_P2P_DISABLED, "Error: Address manager functionality missing or disabled");
1141+
}
1142+
1143+
UniValue ret(UniValue::VOBJ);
1144+
ret.pushKV("new", AddrmanTableToJSON(node.addrman->GetEntries(false)));
1145+
ret.pushKV("tried", AddrmanTableToJSON(node.addrman->GetEntries(true)));
1146+
return ret;
1147+
},
1148+
};
1149+
}
1150+
10821151
void RegisterNetRPCCommands(CRPCTable& t)
10831152
{
10841153
static const CRPCCommand commands[]{
@@ -1099,6 +1168,7 @@ void RegisterNetRPCCommands(CRPCTable& t)
10991168
{"hidden", &addpeeraddress},
11001169
{"hidden", &sendmsgtopeer},
11011170
{"hidden", &getaddrmaninfo},
1171+
{"hidden", &getrawaddrman},
11021172
};
11031173
for (const auto& c : commands) {
11041174
t.appendCommand(c.name, &c);

src/test/fuzz/rpc.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
142142
"getnodeaddresses",
143143
"getpeerinfo",
144144
"getprioritisedtransactions",
145+
"getrawaddrman",
145146
"getrawmempool",
146147
"getrawtransaction",
147148
"getrpcinfo",

test/functional/rpc_net.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import time
1313

1414
import test_framework.messages
15+
from test_framework.netutil import ADDRMAN_NEW_BUCKET_COUNT, ADDRMAN_TRIED_BUCKET_COUNT, ADDRMAN_BUCKET_SIZE
1516
from test_framework.p2p import (
1617
P2PInterface,
1718
P2P_SERVICES,
@@ -67,6 +68,7 @@ def run_test(self):
6768
self.test_addpeeraddress()
6869
self.test_sendmsgtopeer()
6970
self.test_getaddrmaninfo()
71+
self.test_getrawaddrman()
7072

7173
def test_connection_count(self):
7274
self.log.info("Test getconnectioncount")
@@ -388,5 +390,115 @@ def test_getaddrmaninfo(self):
388390
assert_equal(res[net]["tried"], 0)
389391
assert_equal(res[net]["total"], 0)
390392

393+
def test_getrawaddrman(self):
394+
self.log.info("Test getrawaddrman")
395+
node = self.nodes[1]
396+
397+
self.log.debug("Test that getrawaddrman is a hidden RPC")
398+
# It is hidden from general help, but its detailed help may be called directly.
399+
assert "getrawaddrman" not in node.help()
400+
assert "getrawaddrman" in node.help("getrawaddrman")
401+
402+
def check_addr_information(result, expected):
403+
"""Utility to compare a getrawaddrman result entry with an expected entry"""
404+
assert_equal(result["address"], expected["address"])
405+
assert_equal(result["port"], expected["port"])
406+
assert_equal(result["services"], expected["services"])
407+
assert_equal(result["network"], expected["network"])
408+
assert_equal(result["source"], expected["source"])
409+
assert_equal(result["source_network"], expected["source_network"])
410+
# To avoid failing on slow test runners, use a 10s vspan here.
411+
assert_approx(result["time"], time.time(), vspan=10)
412+
413+
def check_getrawaddrman_entries(expected):
414+
"""Utility to compare a getrawaddrman result with expected addrman contents"""
415+
getrawaddrman = node.getrawaddrman()
416+
getaddrmaninfo = node.getaddrmaninfo()
417+
for (table_name, table_info) in expected.items():
418+
assert_equal(len(getrawaddrman[table_name]), len(table_info["entries"]))
419+
assert_equal(len(getrawaddrman[table_name]), getaddrmaninfo["all_networks"][table_name])
420+
421+
for bucket_position in getrawaddrman[table_name].keys():
422+
bucket = int(bucket_position.split("/")[0])
423+
position = int(bucket_position.split("/")[1])
424+
425+
# bucket and position only be sanity checked here as the
426+
# test-addrman isn't deterministic
427+
assert 0 <= int(bucket) < table_info["bucket_count"]
428+
assert 0 <= int(position) < ADDRMAN_BUCKET_SIZE
429+
430+
entry = getrawaddrman[table_name][bucket_position]
431+
expected_entry = list(filter(lambda e: e["address"] == entry["address"], table_info["entries"]))[0]
432+
check_addr_information(entry, expected_entry)
433+
434+
# we expect one addrman new and tried table entry, which were added in a previous test
435+
expected = {
436+
"new": {
437+
"bucket_count": ADDRMAN_NEW_BUCKET_COUNT,
438+
"entries": [
439+
{
440+
"address": "2.0.0.0",
441+
"port": 8333,
442+
"services": 9,
443+
"network": "ipv4",
444+
"source": "2.0.0.0",
445+
"source_network": "ipv4",
446+
}
447+
]
448+
},
449+
"tried": {
450+
"bucket_count": ADDRMAN_TRIED_BUCKET_COUNT,
451+
"entries": [
452+
{
453+
"address": "1.2.3.4",
454+
"port": 8333,
455+
"services": 9,
456+
"network": "ipv4",
457+
"source": "1.2.3.4",
458+
"source_network": "ipv4",
459+
}
460+
]
461+
}
462+
}
463+
464+
self.log.debug("Test that the getrawaddrman contains information about the addresses added in a previous test")
465+
check_getrawaddrman_entries(expected)
466+
467+
self.log.debug("Add one new address to each addrman table")
468+
expected["new"]["entries"].append({
469+
"address": "2803:0:1234:abcd::1",
470+
"services": 9,
471+
"network": "ipv6",
472+
"source": "2803:0:1234:abcd::1",
473+
"source_network": "ipv6",
474+
"port": -1, # set once addpeeraddress is successful
475+
})
476+
expected["tried"]["entries"].append({
477+
"address": "nrfj6inpyf73gpkyool35hcmne5zwfmse3jl3aw23vk7chdemalyaqad.onion",
478+
"services": 9,
479+
"network": "onion",
480+
"source": "nrfj6inpyf73gpkyool35hcmne5zwfmse3jl3aw23vk7chdemalyaqad.onion",
481+
"source_network": "onion",
482+
"port": -1, # set once addpeeraddress is successful
483+
})
484+
485+
port = 0
486+
for (table_name, table_info) in expected.items():
487+
# There's a slight chance that the to-be-added address collides with an already
488+
# present table entry. To avoid this, we increment the port until an address has been
489+
# added. Incrementing the port changes the position in the new table bucket (bucket
490+
# stays the same) and changes both the bucket and the position in the tried table.
491+
while True:
492+
if node.addpeeraddress(address=table_info["entries"][1]["address"], port=port, tried=table_name == "tried")["success"]:
493+
table_info["entries"][1]["port"] = port
494+
self.log.debug(f"Added {table_info['entries'][1]['address']} to {table_name} table")
495+
break
496+
else:
497+
port += 1
498+
499+
self.log.debug("Test that the newly added addresses appear in getrawaddrman")
500+
check_getrawaddrman_entries(expected)
501+
502+
391503
if __name__ == '__main__':
392504
NetTest().main()

0 commit comments

Comments
 (0)