cli: include local ("unroutable") peers in -netinfo table #26584

pull pinheadmz wants to merge 1 commits into bitcoin:master from pinheadmz:netinfo-local changing 1 files +14 −4
  1. pinheadmz commented at 1:55 PM on November 27, 2022: member

    Closes #26579

    The -netinfo dashboard did not list peers that were connected via "unroutable" networks. This included local peers including local-network peers. Personally, I run one bitcoind instance on my network that is used by other services like Wasabi Wallet and LND running on other machines.

    This PR adds an "npr" (not publicly routable) column to the table of networks (ipv4, ipv6, onion, etc) so that every connection to the node is listed, and the totals are accurate as they relate to max inbound and max outbound limits.

    Example connecting in regtest mode to one local and one remote peer:

    Bitcoin Core client v24.99.0-151ce099ea8f-dirty regtest - server 70016/Satoshi:24.99.0/
    
    <->   type   net  mping   ping send recv  txn  blk  hb addrp addrl  age id address         version
     in          npr      0      0   90   90                              1  1 127.0.0.1:59180 70016/Satoshi:24.99.0/
    out manual  ipv4     63     63   84   84         3                    3  0 143.244.175.41  70016/Satoshi:24.0.1/
                         ms     ms  sec  sec  min  min                  min
    
             ipv4    ipv6     npr   total   block  manual
    in          0       0       1       1
    out         1       0       0       1       0       1
    total       1       0       1       2
    
    Local addresses: n/a
    
    
    
  2. DrahtBot commented at 1:55 PM on November 27, 2022: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK jonatack

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #26988 (rpc: Add test-only RPC addrmaninfo for new/tried table address count by stratospher)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  3. jonatack commented at 3:52 PM on November 27, 2022: contributor

    Thanks! I'll have a look today.

  4. in src/bitcoin-cli.cpp:58 in fb0f5d0bd4 outdated
      54 | @@ -55,7 +55,7 @@ static constexpr int DEFAULT_WAIT_CLIENT_TIMEOUT = 0;
      55 |  static const bool DEFAULT_NAMED=false;
      56 |  static const int CONTINUE_EXECUTION=-1;
      57 |  static constexpr int8_t UNKNOWN_NETWORK{-1};
      58 | -static constexpr std::array NETWORKS{"ipv4", "ipv6", "onion", "i2p", "cjdns"};
      59 | +static constexpr std::array NETWORKS{"ipv4", "ipv6", "onion", "i2p", "cjdns", "other"};
    


    jonatack commented at 4:41 PM on November 27, 2022:

    Changing NETWORKS here also affects -addrinfo, so it would need to be updated as well. Otherwise, with this change -addrinfo will print an additional line that always returns "other": 0,.

    Would it make any sense to handle Network::NET_UNROUTABLE and Network::INTERNAL separately? Say, print npr for "not publicly routable" and int for "internal". In theory both could potentially be returned by getpeerinfo, which calls GetNetworkName() for the string value, and the GUI handles both of them in NetworkToQString(). It doesn't seem to make sense that we would ever see an "internal" returned by getpeerinfo (called by -netinfo) or by getnodeaddresses (called by -addrinfo, and getnodeaddresses also raises if called with "not publicly routable" passed as a network), but as a sanity check perhaps handle them like the GUI does.

    Also, the -netinfo help would need to be updated for the net column description.


    pinheadmz commented at 6:57 PM on November 27, 2022:

    updated and force-pushed, i think the logic is cleaner now anyway, by just including the extra categories from GetNetworkName() but only displaying them if peers are actually connected there. Thanks!

  5. in src/bitcoin-cli.cpp:575 in fb0f5d0bd4 outdated
     569 | @@ -571,6 +570,10 @@ class NetinfoRequestHandler : public BaseRequestHandler
     570 |                  reachable_networks.push_back(network_id);
     571 |              }
     572 |          };
     573 | +
     574 | +        reachable_networks.push_back(NETWORKS.size() - 1);
     575 | +        result += strprintf("%8s", "other");
    


    jonatack commented at 4:45 PM on November 27, 2022:

    Just as we only print columns for networks we are using, I think it would be better to only print this column if we have any such peers.


    pinheadmz commented at 6:56 PM on November 27, 2022:

    :+1:

  6. jonatack commented at 5:01 PM on November 27, 2022: contributor

    Concept ACK

  7. pinheadmz force-pushed on Nov 27, 2022
  8. maflcko renamed this:
    cli: include local ("unroutable") peers in -netinfo table
    cli: include local ("unroutable") peers in -netinfo table
    on Nov 28, 2022
  9. DrahtBot added the label Scripts and tools on Nov 28, 2022
  10. maflcko renamed this:
    cli: include local ("unroutable") peers in -netinfo table
    cli: include local ("unroutable") peers in -netinfo table
    on Nov 28, 2022
  11. in src/bitcoin-cli.cpp:60 in c0c45165e3 outdated
      54 | @@ -55,7 +55,10 @@ static constexpr int DEFAULT_WAIT_CLIENT_TIMEOUT = 0;
      55 |  static const bool DEFAULT_NAMED=false;
      56 |  static const int CONTINUE_EXECUTION=-1;
      57 |  static constexpr int8_t UNKNOWN_NETWORK{-1};
      58 | -static constexpr std::array NETWORKS{"ipv4", "ipv6", "onion", "i2p", "cjdns"};
      59 | +// See GetNetworkName() in netbase.cpp
      60 | +static constexpr std::array NETWORKS{"not_publicly_routable", "ipv4", "ipv6", "onion", "i2p", "cjdns", "internal"};
      61 | +static constexpr std::array NETWORK_NAMES{"npr", "ipv4", "ipv6", "onion", "i2p", "cjdns", "int"};
    


    luke-jr commented at 9:29 PM on November 29, 2022:

    NETWORK_SHORTNAMES?

  12. in src/bitcoin-cli.cpp:61 in c0c45165e3 outdated
      54 | @@ -55,7 +55,10 @@ static constexpr int DEFAULT_WAIT_CLIENT_TIMEOUT = 0;
      55 |  static const bool DEFAULT_NAMED=false;
      56 |  static const int CONTINUE_EXECUTION=-1;
      57 |  static constexpr int8_t UNKNOWN_NETWORK{-1};
      58 | -static constexpr std::array NETWORKS{"ipv4", "ipv6", "onion", "i2p", "cjdns"};
      59 | +// See GetNetworkName() in netbase.cpp
      60 | +static constexpr std::array NETWORKS{"not_publicly_routable", "ipv4", "ipv6", "onion", "i2p", "cjdns", "internal"};
      61 | +static constexpr std::array NETWORK_NAMES{"npr", "ipv4", "ipv6", "onion", "i2p", "cjdns", "int"};
      62 | +static constexpr std::array NETWORK_UNREACHABLE{0 /* "not_publicly_routable" */, 6 /* "internal" */};
    


    luke-jr commented at 9:33 PM on November 29, 2022:

    This code looks like it should probably get some refactoring. No reason it can't work with network strings (not ids) and figure out what "unlisted" networks have peers on its own, and doing so would be more future-proof as an added bonus.


    pinheadmz commented at 2:53 PM on November 30, 2022:

    Good idea -- would it make sense to #include <netbase.h> in bitcoin-cli.cpp? Then we can share one single list enum Network as well as GetNetworkName() and ParseNetwork() without re-writing those in bitcoin-cli. I could also add a new function GetNetworkShortName() in netbase.cpp so everything is in one place. If new networks are added in the future, bitcoin-cli.cpp shouldn't need any changes...


    pinheadmz commented at 7:16 PM on November 30, 2022:

    @luke-jr gave this refactor a try in 7dd8b095ada2711487057788206deb7e310b8092


    luke-jr commented at 9:51 PM on December 3, 2022:

    No, I'm saying there shouldn't be an enum Network or network_id at all - just use the strings.


    pinheadmz commented at 1:50 PM on December 4, 2022:

    ok something like std::map<std::string, int> to keep track of count of peers by network string?

  13. pinheadmz force-pushed on Dec 2, 2022
  14. luke-jr commented at 9:51 PM on December 3, 2022: member
    ## Commit message ##
    -    cli: include local ("unroutable") peers in -netinfo table
    +    cli: include local ("unreachable") peers in -netinfo table
    

    Both of these are kinda wrong :|

  15. pinheadmz force-pushed on Dec 8, 2022
  16. pinheadmz commented at 5:09 PM on December 8, 2022: member

    @luke-jr I think I implemented the cleaner refactor you were thinking of in 0d0994a2e2e7b10ee428fde0f6c9235faf2a8e6a

    I also applied that to -addrinfo which does have one drawback, we will now only list networks that come up in the getnodeaddresses RPC call, and not "all" networks, which were previously defined in a hard-coded list. Dunno if @jonatack has any thoughts on that, or if this is even considered a breaking API change ?

  17. fanquake commented at 9:57 AM on December 9, 2022: member

    or if this is even considered a breaking API change ?

    These interfaces have no API or guarantees. This is called out in -netinfo, and the same should hold for -addrinfo:

    This human-readable interface will change regularly and is not intended to be a stable API.

  18. in src/bitcoin-cli.cpp:281 in 0d0994a2e2 outdated
     282 | -            ++counts.at(network_id);
     283 | +            std::string network{node["network"].get_str()};
     284 | +            if (m_counts.find(network) == m_counts.end()) {
     285 | +                m_counts.insert({network, 0});
     286 | +            }
     287 | +            ++m_counts.find(network)->second;
    


    luke-jr commented at 12:21 AM on December 13, 2022:
                ++m_counts[network];
    

    pinheadmz commented at 2:24 PM on December 13, 2022:

    thanks, fixed in bf69a13677680dbf2065e18e3e5c7882ec3aab33

  19. in src/bitcoin-cli.cpp:464 in 0d0994a2e2 outdated
     457 | @@ -470,20 +458,36 @@ class NetinfoRequestHandler : public BaseRequestHandler
     458 |          if (networkinfo["version"].getInt<int>() < 209900) {
     459 |              throw std::runtime_error("-netinfo requires bitcoind server to be running v0.21.0 and up");
     460 |          }
     461 | +        // Initialize the peer-count-by-network-type map with all the reachable networks (always display)
     462 | +        for (const UniValue& network : networkinfo["networks"].getValues()) {
     463 | +            if (!network["reachable"].get_bool())
     464 | +                continue;
    


    luke-jr commented at 12:24 AM on December 13, 2022:
                if (!network["reachable"].get_bool()) {
                    continue;
                }
    

    pinheadmz commented at 2:24 PM on December 13, 2022:

    thanks, fixed in bf69a13677680dbf2065e18e3e5c7882ec3aab33

  20. in src/bitcoin-cli.cpp:371 in 0d0994a2e2 outdated
     367 | @@ -373,16 +368,9 @@ class NetinfoRequestHandler : public BaseRequestHandler
     368 |  {
     369 |  private:
     370 |      static constexpr uint8_t MAX_DETAIL_LEVEL{4};
     371 | -    std::array<std::array<uint16_t, NETWORKS.size() + 1>, 3> m_counts{{{}}}; //!< Peer counts by (in/out/total, networks/total)
     372 | +    std::map<std::string, std::array<int, 3>> m_counts; //!< Peer counts by network type (in / out / total)
    


    luke-jr commented at 12:32 AM on December 13, 2022:

    Total is just in+out, and turning the std::array into a std::pair gets default zero initialisation, which would be useful later. We only need total one place, so IMO we should just sum it there rather than counting one by one.


    pinheadmz commented at 2:25 PM on December 13, 2022:

    fixed in bf69a13677680dbf2065e18e3e5c7882ec3aab33 -- the only messy part was that std::pair values can't be retrieved by variable like i in the for loop, so I put a switch in there so that 0 = first and 1 = second, I dunno if that's best.

  21. in src/bitcoin-cli.cpp:515 in 0d0994a2e2 outdated
     509 | @@ -506,7 +510,10 @@ class NetinfoRequestHandler : public BaseRequestHandler
     510 |                  const bool is_addr_relay_enabled{peer["addr_relay_enabled"].isNull() ? false : peer["addr_relay_enabled"].get_bool()};
     511 |                  const bool is_bip152_hb_from{peer["bip152_hb_from"].get_bool()};
     512 |                  const bool is_bip152_hb_to{peer["bip152_hb_to"].get_bool()};
     513 | -                m_peers.push_back({addr, sub_version, conn_type, network, age, min_ping, ping, addr_processed, addr_rate_limited, last_blck, last_recv, last_send, last_trxn, peer_id, mapped_as, version, is_addr_relay_enabled, is_bip152_hb_from, is_bip152_hb_to, is_outbound, is_tx_relay});
     514 | +                std::string name = network;
     515 | +                if (NETWORK_SHORTNAMES.find(name) != NETWORK_SHORTNAMES.end())
     516 | +                    name = NETWORK_SHORTNAMES.find(name)->second;
    


    luke-jr commented at 12:37 AM on December 13, 2022:

    Needs braces at least, but it would be nice to avoid finding twice too.


    pinheadmz commented at 2:25 PM on December 13, 2022:

    thanks fixed in bf69a13677680dbf2065e18e3e5c7882ec3aab33

  22. in src/bitcoin-cli.cpp:571 in 0d0994a2e2 outdated
     577 | -                result += strprintf("%8s", network_name); // column header
     578 | -                reachable_networks.push_back(network_id);
     579 | -            }
     580 | -        };
     581 | -        result += "   total   block";
     582 | +        for (const auto& type : m_counts) {
    


    luke-jr commented at 12:40 AM on December 13, 2022:

    Do we want to sort the networks in some particular order?


    pinheadmz commented at 2:28 PM on December 13, 2022:

    the networks will be sorted by reachable networks first (ipv4, ipv6) followed by any non-reachable networks we still have peers for (npr, i.e. local). The order of reachable networks will be provided by rpc getnetworkinfo which I think ultimately comes form the order of network names in netbase.cpp


    luke-jr commented at 3:21 AM on December 14, 2022:

    std::map sorts alphabetically, IIRC?

  23. luke-jr changes_requested
  24. jonatack commented at 3:32 PM on December 13, 2022: contributor

    Not sure I'm keen on the refactorings; are they necessary? It looks like they are trading an efficient data structure for a less-efficient one and moving additional logic/processing work into the loop.

  25. luke-jr commented at 3:20 AM on December 14, 2022: member

    I'm not sure a std::map is less efficient than mapping to arbitrary [hash-like] values, the latter of which breaks/has to be kept updated any time the software changes. The refactor just makes the interface generic so it works without a hard-coded network list.

  26. in src/bitcoin-cli.cpp:481 in bf69a13677 outdated
     482 | +            m_counts[network];
     483 | +            if (is_outbound) {
     484 | +                ++m_counts.find(network)->second.second;
     485 | +            } else {
     486 | +                ++m_counts.find(network)->second.first;
     487 | +            }
    


    luke-jr commented at 3:25 AM on December 14, 2022:
                ++std::get<is_outbound ? 1 : 0>(m_counts[network]);
    

    pinheadmz commented at 6:33 PM on December 15, 2022:

    I didn't think you can use a variable in std::get< ? > ?

    bitcoin-cli.cpp:476:15: error: no matching function for call to 'get'
     ++std::get<is_outbound ? 1 : 0>(m_counts[network]);
    

    https://stackoverflow.com/questions/43621098/c-stdgetvariable-fails

  27. in src/bitcoin-cli.cpp:508 in bf69a13677 outdated
     500 | @@ -506,7 +501,12 @@ class NetinfoRequestHandler : public BaseRequestHandler
     501 |                  const bool is_addr_relay_enabled{peer["addr_relay_enabled"].isNull() ? false : peer["addr_relay_enabled"].get_bool()};
     502 |                  const bool is_bip152_hb_from{peer["bip152_hb_from"].get_bool()};
     503 |                  const bool is_bip152_hb_to{peer["bip152_hb_to"].get_bool()};
     504 | -                m_peers.push_back({addr, sub_version, conn_type, network, age, min_ping, ping, addr_processed, addr_rate_limited, last_blck, last_recv, last_send, last_trxn, peer_id, mapped_as, version, is_addr_relay_enabled, is_bip152_hb_from, is_bip152_hb_to, is_outbound, is_tx_relay});
     505 | +                std::string name = network;
     506 | +                const std::map<std::string, std::string>::const_iterator it = NETWORK_SHORTNAMES.find(name);
     507 | +                if (it != NETWORK_SHORTNAMES.end()) {
     508 | +                    name = it->second;
     509 | +                }
    


    luke-jr commented at 3:31 AM on December 14, 2022:
                    const std::map<std::string, std::string>::const_iterator it = NETWORK_SHORTNAMES.find(network);
                    const std::string& network_display_name = (it == NETWORK_SHORTNAMES.end()) ? network : it->second;
    

    luke-jr commented at 3:46 AM on December 14, 2022:

    Or maybe add a nice utility function like:

    template <typename M>
    const M::mapped_type map_at_or(const M& map, const M::key_type& key, const M::mapped_type& default) {
        const auto it = map.find(key);
        if (it == map.end()) return default;
        return it->second;
    }
    

    Then we can do:

                    const std::string& network_display_name = map_at_or(NETWORK_SHORTNAMES, network, network);
    
  28. in src/bitcoin-cli.cpp:591 in bf69a13677 outdated
     597 | +                        count = std::get<1>(type.second); // outbound peers by network
     598 | +                        break;
     599 | +                    case 2:
     600 | +                        count = std::get<0>(type.second) + std::get<1>(type.second); // total peers by network
     601 | +                        break;
     602 | +                }
    


    luke-jr commented at 3:52 AM on December 14, 2022:
                for (const auto& [network, node_counts] : m_counts) {
                    const int count = (i == 2) ? (node_counts.first + node_counts.second) : std::get<i>(node_counts);
    
  29. luke-jr changes_requested
  30. pinheadmz force-pushed on Dec 16, 2022
  31. pinheadmz commented at 7:35 PM on December 16, 2022: member

    I reset this PR back to the original commit (with Luke's nit addressed). I'd like to try and just get the minimal code changes merged to improve the feature. Then if you guys like we can talk about ways to refactor if necessary

  32. pinheadmz force-pushed on Feb 1, 2023
  33. jonatack commented at 9:05 PM on February 1, 2023: contributor

    Tested ACK with suggestions for clarity or coherence with the existing code.

    <details><summary>tested by adding the following 3 lines</summary><p>

    @@ -562,6 +562,10 @@ public:
                 result += strprintf("                     ms     ms  sec  sec  min  min                %*s\n\n", m_max_age_length, "min");
             }
     
    +        m_counts.at(0).at(0) += 1;
    +        m_counts.at(1).at(0) += 2;
    +        m_counts.at(2).at(0) += 3;
    +
             // Report peer connection totals by type.
    

    </p></details>

    <details><summary>suggestions diff</summary><p>

    @@ -58,7 +58,7 @@ static constexpr int8_t UNKNOWN_NETWORK{-1};
     // See GetNetworkName() in netbase.cpp
     static constexpr std::array NETWORKS{"not_publicly_routable", "ipv4", "ipv6", "onion", "i2p", "cjdns", "internal"};
     static constexpr std::array NETWORK_SHORT_NAMES{"npr", "ipv4", "ipv6", "onion", "i2p", "cjdns", "int"};
    -static constexpr std::array NETWORK_UNREACHABLE{0 /* "not_publicly_routable" */, 6 /* "internal" */};
    +static constexpr std::array UNREACHABLE_NETWORK_IDS{/*not_publicly_routable*/0, /*internal*/6};
     
     /** Default number of blocks to generate for RPC generatetoaddress. */
     static const std::string DEFAULT_NBLOCKS = "1";
    @@ -575,10 +575,10 @@ public:
             for (const UniValue& network : networkinfo["networks"].getValues()) {
                 if (network["reachable"].get_bool()) {
                     const std::string& network_name{network["name"].get_str()};
                     const int8_t network_id{NetworkStringToId(network_name)};
                     if (network_id == UNKNOWN_NETWORK) continue;
                     result += strprintf("%8s", network_name); // column header
                     reachable_networks.push_back(network_id);
                 }
             };
     
    -        for (const size_t index : NETWORK_UNREACHABLE) {
    -            if (m_counts.at(2).at(index)) {
    -                reachable_networks.push_back(index);
    -                result += strprintf("%8s", NETWORK_SHORT_NAMES[index]);
    +        for (size_t network_id : UNREACHABLE_NETWORK_IDS) {
    +            if (m_counts.at(2).at(network_id)) {
    +                result += strprintf("%8s", NETWORK_SHORT_NAMES.at(network_id)); // column header
    +                reachable_networks.push_back(network_id);
                 }
             }
    

    </p></details>

  34. pinheadmz force-pushed on Feb 1, 2023
  35. pinheadmz commented at 9:21 PM on February 1, 2023: member

    @jonatack thank you! applied suggestions and f-pushed

  36. in src/bitcoin-cli.cpp:582 in ea4fbc5277 outdated
     578 | +        for (const size_t network_id : UNREACHABLE_NETWORK_IDS) {
     579 | +            if (m_counts.at(2).at(network_id)) {
     580 | +                result += strprintf("%8s", NETWORK_SHORT_NAMES.at(network_id)); // column header
     581 | +                reachable_networks.push_back(network_id);
     582 | +            }
     583 | +        }
    


    jonatack commented at 9:43 PM on February 1, 2023:

    Can ignore, the following version might be more idiomatic C++.

    -        for (const size_t network_id : UNREACHABLE_NETWORK_IDS) {
    -            if (m_counts.at(2).at(network_id)) {
    -                result += strprintf("%8s", NETWORK_SHORT_NAMES.at(network_id)); // column header
    -                reachable_networks.push_back(network_id);
    -            }
    +        for (size_t network_id : UNREACHABLE_NETWORK_IDS) {
    +            if (m_counts.at(2).at(network_id) == 0) continue;
    +            result += strprintf("%8s", NETWORK_SHORT_NAMES.at(network_id)); // column header
    +            reachable_networks.push_back(network_id);
             }
    

    pinheadmz commented at 12:44 PM on February 2, 2023:

    Thanks! both suggestions applied.

  37. jonatack commented at 9:44 PM on February 1, 2023: contributor

    ACK ea4fbc5277c6fdf43cf675c8cc612f4821f55868, modulo the following change so that -addrinfo prints only the relevant networks:

    @@ -292,7 +292,7 @@ public:
             // Prepare result to return to user.
             UniValue result{UniValue::VOBJ}, addresses{UniValue::VOBJ};
             uint64_t total{0}; // Total address count
    -        for (size_t i = 0; i < NETWORKS.size(); ++i) {
    +        for (size_t i = 1; i < NETWORKS.size() - 1; ++i) {
    

    Tested this further with the following change on a running node:

    --- a/src/bitcoin-cli.cpp
    +++ b/src/bitcoin-cli.cpp
    @@ -478,7 +478,7 @@ public:
             // Count peer connection totals, and if DetailsRequested(), store peer data in a vector of structs.
             for (const UniValue& peer : batch[ID_PEERINFO]["result"].getValues()) {
                 const std::string network{peer["network"].get_str()};
    -            const int8_t network_id{NetworkStringToId(network)};
    +            const int8_t network_id = NetworkStringToId(network) % 2 ? 0 : 6;
    

    <img width="482" alt="Screenshot 2023-02-01 at 13 36 12" src="https://user-images.githubusercontent.com/2415484/216168855-70c6437d-9efa-40fa-ae3a-bb0dfd03cf6e.png">

  38. pinheadmz force-pushed on Feb 2, 2023
  39. cli: include local ("unreachable") peers in -netinfo table 77192c9598
  40. pinheadmz force-pushed on Feb 2, 2023
  41. jonatack commented at 8:10 PM on February 2, 2023: contributor

    Re-tested ACK 77192c959816dc8daee138d88bd6f3250ce3bdb6

  42. jonatack commented at 3:20 PM on February 13, 2023: contributor

    This is a small, contained, non-risky change and seems fine to merge.

  43. maflcko merged this on Feb 15, 2023
  44. maflcko closed this on Feb 15, 2023

  45. sidhujag referenced this in commit 9789149df4 on Feb 15, 2023
  46. bitcoin locked this on Feb 15, 2024

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-05-03 15:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me