rpc: Add exportasmap RPC

This commit is contained in:
Fabian Jahr
2025-11-21 00:52:31 +01:00
parent d3a40dd9de
commit 8cb2d926b4
3 changed files with 96 additions and 7 deletions

View File

@@ -9,12 +9,17 @@
#include <banman.h>
#include <chainparams.h>
#include <clientversion.h>
#include <common/args.h>
#include <core_io.h>
#include <hash.h>
#include <net_permissions.h>
#include <net_processing.h>
#include <net_types.h>
#include <netbase.h>
#include <node/context.h>
#ifdef ENABLE_EMBEDDED_ASMAP
#include <node/data/ip_asn.dat.h>
#endif
#include <node/protocol_version.h>
#include <node/warnings.h>
#include <policy/settings.h>
@@ -25,6 +30,7 @@
#include <rpc/util.h>
#include <sync.h>
#include <univalue.h>
#include <util/asmap.h>
#include <util/chaintype.h>
#include <util/strencodings.h>
#include <util/string.h>
@@ -1116,6 +1122,59 @@ static RPCMethod getaddrmaninfo()
};
}
static RPCMethod exportasmap()
{
return RPCMethod{
"exportasmap",
"Export the embedded ASMap data to a file. Any existing file at the path will be overwritten.\n",
{
{"path", RPCArg::Type::STR, RPCArg::Optional::NO, "Path to the output file. If relative, will be prefixed by datadir."},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::STR, "path", "the absolute path that the ASMap data was written to"},
{RPCResult::Type::NUM, "bytes_written", "the number of bytes written to the file"},
{RPCResult::Type::STR_HEX, "file_hash", "the SHA256 hash of the exported ASMap data"},
}
},
RPCExamples{
HelpExampleCli("exportasmap", "\"asmap.dat\"") + HelpExampleRpc("exportasmap", "\"asmap.dat\"")},
[&](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue {
#ifndef ENABLE_EMBEDDED_ASMAP
throw JSONRPCError(RPC_MISC_ERROR, "No embedded ASMap data available");
#else
if (node::data::ip_asn.empty() || !CheckStandardAsmap(node::data::ip_asn)) {
throw JSONRPCError(RPC_MISC_ERROR, "Embedded ASMap data appears to be corrupted");
}
const ArgsManager& args{EnsureAnyArgsman(request.context)};
const fs::path export_path{fsbridge::AbsPathJoin(args.GetDataDirNet(), fs::u8path(self.Arg<std::string_view>("path")))};
AutoFile file{fsbridge::fopen(export_path, "wb")};
if (file.IsNull()) {
throw JSONRPCError(RPC_MISC_ERROR, strprintf("Failed to open asmap file: %s", fs::PathToString(export_path)));
}
file << node::data::ip_asn;
if (file.fclose() != 0) {
throw JSONRPCError(RPC_MISC_ERROR, strprintf("Failed to close asmap file: %s", fs::PathToString(export_path)));
}
HashWriter hasher;
hasher.write(node::data::ip_asn);
UniValue result(UniValue::VOBJ);
result.pushKV("path", export_path.utf8string());
result.pushKV("bytes_written", (uint64_t)node::data::ip_asn.size());
result.pushKV("file_hash", HexStr(hasher.GetSHA256()));
return result;
#endif
},
};
}
UniValue AddrmanEntryToJSON(const AddrInfo& info, const CConnman& connman)
{
UniValue ret(UniValue::VOBJ);
@@ -1210,6 +1269,7 @@ void RegisterNetRPCCommands(CRPCTable& t)
{"network", &setnetworkactive},
{"network", &getnodeaddresses},
{"network", &getaddrmaninfo},
{"network", &exportasmap},
{"hidden", &addconnection},
{"hidden", &addpeeraddress},
{"hidden", &sendmsgtopeer},

View File

@@ -78,15 +78,16 @@ const std::vector<std::string> RPC_COMMANDS_NOT_SAFE_FOR_FUZZING{
"dumptxoutset", // avoid writing to disk
"enumeratesigners",
"echoipc", // avoid assertion failure (Assertion `"EnsureAnyNodeContext(request.context).init" && check' failed.)
"exportasmap", // avoid writing to disk
"generatetoaddress", // avoid prohibitively slow execution (when `num_blocks` is large)
"generatetodescriptor", // avoid prohibitively slow execution (when `nblocks` is large)
"gettxoutproof", // avoid prohibitively slow execution
"importmempool", // avoid reading from disk
"loadtxoutset", // avoid reading from disk
"loadwallet", // avoid reading from disk
"savemempool", // disabled as a precautionary measure: may take a file path argument in the future
"setban", // avoid DNS lookups
"stop", // avoid shutdown state
"importmempool", // avoid reading from disk
"loadtxoutset", // avoid reading from disk
"loadwallet", // avoid reading from disk
"savemempool", // disabled as a precautionary measure: may take a file path argument in the future
"setban", // avoid DNS lookups
"stop", // avoid shutdown state
};
// RPC commands which are safe for fuzzing.

View File

@@ -11,11 +11,15 @@ with missing and unparseable files.
The tests are order-independent.
"""
import hashlib
import os
import shutil
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal
from test_framework.util import (
assert_equal,
assert_raises_rpc_error,
)
ASMAP = 'src/test/data/asmap.raw' # path to unit test skeleton asmap
VERSION = 'bafc9da308f45179443bd1d22325400ac9104f741522d003e3fac86700f68895'
@@ -124,6 +128,29 @@ class AsmapTest(BitcoinTestFramework):
asns.append(asn)
assert_equal(len(asns), 3)
def test_export_embedded_asmap(self):
self.log.info('Test exportasmap RPC')
export_path = os.path.join(self.datadir, "asmap.dat")
if not self.is_embedded_asmap_compiled():
assert_raises_rpc_error(-1, "No embedded ASMap data available", self.node.exportasmap, export_path)
return
# Relative paths are resolved against the datadir.
result = self.node.exportasmap("asmap.dat")
assert_equal(result["path"], export_path)
with open(export_path, 'rb') as f:
data = f.read()
assert_equal(result["bytes_written"], len(data))
# Added in https://github.com/bitcoin/bitcoin/pull/34696
expected_hash = "478d61986c59365cf86cd244485bbbe76a9ca0c630864717286dd19949879074"
assert_equal(hashlib.sha256(data).hexdigest(), expected_hash)
assert_equal(result["file_hash"], expected_hash)
os.remove(export_path)
def run_test(self):
self.node = self.nodes[0]
self.datadir = self.node.chain_path
@@ -139,6 +166,7 @@ class AsmapTest(BitcoinTestFramework):
self.test_asmap_with_missing_file()
self.test_empty_asmap()
self.test_asmap_health_check()
self.test_export_embedded_asmap()
if __name__ == '__main__':