diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index cf5e58642ef..5b19de1054e 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -9,12 +9,17 @@ #include #include #include +#include #include +#include #include #include #include #include #include +#ifdef ENABLE_EMBEDDED_ASMAP +#include +#endif #include #include #include @@ -25,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -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("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}, diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index cd7f49872b6..f0362db2f41 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -78,15 +78,16 @@ const std::vector 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. diff --git a/test/functional/feature_asmap.py b/test/functional/feature_asmap.py index 310788f2cbf..5debdfbe408 100755 --- a/test/functional/feature_asmap.py +++ b/test/functional/feature_asmap.py @@ -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__':