fuzz: call lookup functions before calling Ban #27935

pull brunoerg wants to merge 2 commits into bitcoin:master from brunoerg:2023-06-fuzz-banman-ban changing 2 files +18 −6
  1. brunoerg commented at 6:55 pm on June 22, 2023: contributor

    Fixes #27924

    To not have any discrepancy, it’s required to call lookup functions before calling Ban. If we don’t do it, the assertion assert(banmap == banmap_read); may fail because BanMapFromJson will call LookupSubNet and cause the discrepancy between the banned and the loaded one. It happens especially in MacOS (#27924).

    Also, calling lookup functions before banning is what RPC setban does.

  2. DrahtBot commented at 6:55 pm on June 22, 2023: contributor

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

    Code Coverage

    For detailed information about the code coverage, see the test coverage report.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK maflcko, dergoegge
    Stale ACK jonatack

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

  3. DrahtBot added the label Tests on Jun 22, 2023
  4. brunoerg renamed this:
    fuzz: call `LookupSubNet` before calling `Ban`
    fuzz: call `LookupSubNet` before calling `Ban` with a subnet
    on Jun 22, 2023
  5. brunoerg force-pushed on Jun 22, 2023
  6. brunoerg force-pushed on Jun 22, 2023
  7. DrahtBot added the label CI failed on Jun 22, 2023
  8. brunoerg force-pushed on Jun 26, 2023
  9. DrahtBot removed the label CI failed on Jun 26, 2023
  10. maflcko commented at 2:15 pm on June 29, 2023: member
    Could rebase on master to remove export FUZZ_TESTS_CONFIG="--exclude banman" # [#27924](/bitcoin-bitcoin/27924/)?
  11. brunoerg commented at 5:56 pm on June 29, 2023: contributor

    Could rebase on master to remove export FUZZ_TESTS_CONFIG="–exclude banman" # #27924?

    Sure.

  12. brunoerg force-pushed on Jun 29, 2023
  13. brunoerg commented at 1:46 pm on June 30, 2023: contributor
    Force-pushed to remove “–exclude banman” in Mac
  14. fanquake requested review from dergoegge on Jun 30, 2023
  15. DrahtBot added the label Needs rebase on Aug 15, 2023
  16. brunoerg force-pushed on Aug 16, 2023
  17. brunoerg commented at 1:17 pm on August 16, 2023: contributor
    Rebased.
  18. DrahtBot removed the label Needs rebase on Aug 16, 2023
  19. dergoegge commented at 9:10 am on August 17, 2023: member
    This still crashes for me on the following input /r8BAAAC/9LcAF8gMiWADQUA//////////////8AN1wNXMnJyQ3///8Afv///wABAAAAgAAAAADPvg==
  20. brunoerg commented at 12:41 pm on August 17, 2023: contributor
    I’m investigating it.
  21. brunoerg marked this as a draft on Aug 17, 2023
  22. brunoerg force-pushed on Aug 17, 2023
  23. brunoerg marked this as ready for review on Aug 17, 2023
  24. brunoerg renamed this:
    fuzz: call `LookupSubNet` before calling `Ban` with a subnet
    fuzz: call lookup functions before calling `Ban`
    on Aug 17, 2023
  25. brunoerg commented at 2:15 pm on August 17, 2023: contributor

    Thanks for reviewing, @dergoegge. I noted that we also need to call LookupHost when banning with CNetAddr. Force-pushed doing it and updated the PR description and title.

    Also, this way we’re closer to the reality (following what RPC does to set it).

  26. DrahtBot added the label CI failed on Aug 17, 2023
  27. DrahtBot removed the label CI failed on Aug 18, 2023
  28. in src/test/fuzz/banman.cpp:74 in f4bd8f204e outdated
    67@@ -68,12 +68,19 @@ FUZZ_TARGET(banman, .init = initialize_banman)
    68             CallOneOf(
    69                 fuzzed_data_provider,
    70                 [&] {
    71-                    ban_man.Ban(ConsumeNetAddr(fuzzed_data_provider),
    72+                    const std::string name{fuzzed_data_provider.ConsumeRandomLengthString(512)};
    73+                    const std::optional<CNetAddr> addr{LookupHost(name, /*fAllowLookup=*/false)};
    74+                    if (!addr.has_value() || !addr->IsValid()) return;
    75+                    ban_man.Ban(addr.value(),
    


    jonatack commented at 10:57 pm on September 8, 2023:

    It looks like you can drop the IsValid() checks in your updated fuzz cases?

    nit, I think this would be clearer and use the same structure for both of your updated fuzz cases (e.g. use return for neither, or for both):

    0-                    if (!addr.has_value() || !addr->IsValid()) return;
    1-                    ban_man.Ban(addr.value(),
    2-                                ConsumeBanTimeOffset(fuzzed_data_provider), fuzzed_data_provider.ConsumeBool());
    3+                    if (addr.has_value() && addr->IsValid()) {
    4+                        ban_man.Ban(addr.value(), ConsumeBanTimeOffset(fuzzed_data_provider), fuzzed_data_provider.ConsumeBool());
    5+                    }
    

    brunoerg commented at 2:16 am on September 9, 2023:
    Yes, it makes sense. Updated it.
  29. jonatack commented at 11:00 pm on September 8, 2023: member

    ACK f4bd8f204edfc0ca6f0477d78c95c548c3cb50b6

    Happy to re-ack if you take the suggestion.

  30. brunoerg force-pushed on Sep 9, 2023
  31. in src/test/fuzz/banman.cpp:73 in c92a377139 outdated
    67@@ -68,12 +68,18 @@ FUZZ_TARGET(banman, .init = initialize_banman)
    68             CallOneOf(
    69                 fuzzed_data_provider,
    70                 [&] {
    71-                    ban_man.Ban(ConsumeNetAddr(fuzzed_data_provider),
    72-                                ConsumeBanTimeOffset(fuzzed_data_provider), fuzzed_data_provider.ConsumeBool());
    73+                    const std::string name{fuzzed_data_provider.ConsumeRandomLengthString(512)};
    74+                    const std::optional<CNetAddr> addr{LookupHost(name, /*fAllowLookup=*/false)};
    75+                    if (addr.has_value()) {
    


    jonatack commented at 3:22 am on September 9, 2023:

    Thanks! However, the updates are in the wrong commit.

    Could optionally define addr in the conditional (as reference to const, I think); feel free to ignore.

    0                 fuzzed_data_provider,
    1                 [&] {
    2                     const std::string name{fuzzed_data_provider.ConsumeRandomLengthString(512)};
    3-                    const std::optional<CNetAddr> addr{LookupHost(name, /*fAllowLookup=*/false)};
    4-                    if (addr.has_value()) {
    5+                    if (const std::optional<CNetAddr>& addr = LookupHost(name, /*fAllowLookup=*/false)) {
    6                         ban_man.Ban(addr.value(), ConsumeBanTimeOffset(fuzzed_data_provider), fuzzed_data_provider.ConsumeBool());
    7                     }
    8                 },
    

    brunoerg commented at 3:51 am on September 9, 2023:
    Oh my bad for putting the updates in the wrong commit, just fixed it and addressed your suggestion.
  32. brunoerg force-pushed on Sep 9, 2023
  33. DrahtBot added the label CI failed on Sep 9, 2023
  34. jonatack commented at 1:27 pm on September 9, 2023: member
    ACK f107b60865412fb3d3e4ce5dd3c6d89dd29ec644
  35. DrahtBot removed the label CI failed on Sep 10, 2023
  36. dergoegge commented at 10:27 am on September 14, 2023: member
    I’m not sure if this is the right approach to fixing this. If we always expect assert(banmap == banmap_read); to hold then BanMan::Ban should probably internalize the lookup call. Otherwise we just end up calling it before every Ban call.
  37. dergoegge approved
  38. dergoegge commented at 9:23 am on October 23, 2023: member

    Code review ACK f107b60865412fb3d3e4ce5dd3c6d89dd29ec644

    I think this would also fix #27071 (comment).

    What I suggested above would require more changes, so maybe we just go with what is here now…

  39. fanquake requested review from maflcko on Oct 23, 2023
  40. in src/test/fuzz/banman.cpp:81 in f107b60865 outdated
    76+                    }
    77                 },
    78                 [&] {
    79-                    ban_man.Ban(ConsumeSubNet(fuzzed_data_provider),
    80-                                ConsumeBanTimeOffset(fuzzed_data_provider), fuzzed_data_provider.ConsumeBool());
    81+                    const std::string name{fuzzed_data_provider.ConsumeRandomLengthString(512)};
    


    maflcko commented at 9:29 am on October 23, 2023:

    nit: Does the string representation make it harder for the fuzz engine to reach coverage?

    Previously the engine could just copy some bytes and then trivially trigger the Ban and IsBanned code path. Now it would need to figure out two different representations of the same thing?


    brunoerg commented at 12:40 pm on October 23, 2023:

    Does the string representation make it harder for the fuzz engine to reach coverage?

    Hm, yes, perhaps an alternative to facilitate it (didnt test yet, just wondering):

     0+        CSubNet subnet;
     1+        CNetAddr net_addr;
     2         LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 300)
     3         {
     4             CallOneOf(
     5                 fuzzed_data_provider,
     6                 [&] {
     7-                    const std::string name{fuzzed_data_provider.ConsumeRandomLengthString(512)};
     8-                    if (const std::optional<CNetAddr>& addr{LookupHost(name, /*fAllowLookup=*/false)}) {
     9-                        ban_man.Ban(addr.value(), ConsumeBanTimeOffset(fuzzed_data_provider), fuzzed_data_provider.ConsumeBool());
    10+                    CSubNet new_subnet{ConsumeSubNet(fuzzed_data_provider)};
    11+                    if (LookupSubNet(new_subnet.ToString(), new_subnet)) {
    12+                        subnet = new_subnet;
    13                     }
    14                 },
    15                 [&] {
    16-                    const std::string name{fuzzed_data_provider.ConsumeRandomLengthString(512)};
    17-                    CSubNet subnet;
    18-                    if (LookupSubNet(name, subnet)) {
    19-                        ban_man.Ban(subnet, ConsumeBanTimeOffset(fuzzed_data_provider), fuzzed_data_provider.ConsumeBool());
    20+                    CNetAddr new_netaddr{ConsumeNetAddr(fuzzed_data_provider)};
    21+                    if (const std::optional<CNetAddr>& addr{LookupHost(new_netaddr.ToStringAddr(), /*fAllowLookup=*/false)}) {
    22+                        net_addr = addr.value();
    23                     }
    24                 },
    25+                [&] {
    26+                    ban_man.Ban(net_addr, ConsumeBanTimeOffset(fuzzed_data_provider), fuzzed_data_provider.ConsumeBool());
    27+                },
    28+                [&] {
    29+                    ban_man.Ban(subnet, ConsumeBanTimeOffset(fuzzed_data_provider), fuzzed_data_provider.ConsumeBool());
    30+                },
    31                 [&] {
    32                     ban_man.ClearBanned();
    33                 },
    34                 [&] {
    35-                    ban_man.IsBanned(ConsumeNetAddr(fuzzed_data_provider));
    36+                    ban_man.IsBanned(net_addr);
    37                 },
    38                 [&] {
    39-                    ban_man.IsBanned(ConsumeSubNet(fuzzed_data_provider));
    40+                    ban_man.IsBanned(subnet);
    41                 },
    42                 [&] {
    43-                    ban_man.Unban(ConsumeNetAddr(fuzzed_data_provider));
    44+                    ban_man.Unban(net_addr);
    45                 },
    46                 [&] {
    47-                    ban_man.Unban(ConsumeSubNet(fuzzed_data_provider));
    48+                    ban_man.Unban(subnet);
    49                 },
    50                 [&] {
    51                     banmap_t banmap;
    52@@ -103,7 +110,7 @@ FUZZ_TARGET(banman, .init = initialize_banman)
    53                     ban_man.DumpBanlist();
    54                 },
    55                 [&] {
    56-                    ban_man.Discourage(ConsumeNetAddr(fuzzed_data_provider));
    57+                    ban_man.Discourage(net_addr);
    58                 });
    59         }
    

    dergoegge commented at 1:18 pm on October 23, 2023:
    If you’re gonna change this then my suggestion from #27935 (comment) is probably simpler and doesn’t touch the fuzz test at all.

    vasild commented at 1:53 pm on October 23, 2023:

    Would it be possible to still call ConsumeSubNet() but check if the returned CSubNet object IsValid() before feeding it to Ban()?

    ConsumeSubNet() could generate nonsensical stuff like 2bqghnldu6mcug4pikzprwhtjjnsyederctvci6klcwzepnjd46ikjyd.onion/16 which is later an invalid CSubNet whose constructor only assumes IPv4 or IPv6 subnets valid. This is the reason also for #27071 (comment) (it generates fc00::%538976288/6 IPv6 which later gets converted to CJDNS and rightfully turns out to be an invalid subnet because it is not IPv4 or IPv6).

    Or change ConsumeSubNet() to only produce valid stuff?


    brunoerg commented at 6:29 pm on October 23, 2023:

    If you’re gonna change this then my suggestion from #27935 (comment) is probably simpler and doesn’t touch the fuzz test at all.

    like this, @dergoegge?

     0diff --git a/src/banman.cpp b/src/banman.cpp
     1index a96b7e3c53..e42dc6f9c6 100644
     2--- a/src/banman.cpp
     3+++ b/src/banman.cpp
     4@@ -8,6 +8,7 @@
     5 #include <common/system.h>
     6 #include <logging.h>
     7 #include <netaddress.h>
     8+#include <netbase.h>
     9 #include <node/interface_ui.h>
    10 #include <sync.h>
    11 #include <util/time.h>
    12@@ -116,7 +117,13 @@ bool BanMan::IsBanned(const CSubNet& sub_net)
    13 
    14 void BanMan::Ban(const CNetAddr& net_addr, int64_t ban_time_offset, bool since_unix_epoch)
    15 {
    16-    CSubNet sub_net(net_addr);
    17+    CNetAddr netaddr;
    18+    if (const std::optional<CNetAddr>& addr{LookupHost(net_addr.ToStringAddr(), /*fAllowLookup=*/false)}) {
    19+        netaddr = addr.value();
    20+    } else {
    21+        return;
    22+    }
    23+    CSubNet sub_net(netaddr);
    24     Ban(sub_net, ban_time_offset, since_unix_epoch);
    25 }
    26 
    27@@ -128,6 +135,9 @@ void BanMan::Discourage(const CNetAddr& net_addr)
    28 
    29 void BanMan::Ban(const CSubNet& sub_net, int64_t ban_time_offset, bool since_unix_epoch)
    30 {
    31+    CSubNet subnet;
    32+    LookupSubNet(sub_net.ToString(), subnet);
    33+
    34     CBanEntry ban_entry(GetTime());
    35 
    36     int64_t normalized_ban_time_offset = ban_time_offset;
    37@@ -140,8 +150,8 @@ void BanMan::Ban(const CSubNet& sub_net, int64_t ban_time_offset, bool since_uni
    38 
    39     {
    40         LOCK(m_cs_banned);
    41-        if (m_banned[sub_net].nBanUntil < ban_entry.nBanUntil) {
    42-            m_banned[sub_net] = ban_entry;
    43+        if (m_banned[subnet].nBanUntil < ban_entry.nBanUntil) {
    44+            m_banned[subnet] = ban_entry;
    45             m_is_dirty = true;
    46         } else
    47             return;
    

    maflcko commented at 7:59 am on October 24, 2023:
    I think it is fine to only fix the fuzz test, if the issue is limited to the fuzz test? #27935 (review) lgtm, but I haven’t reviewed it.

    maflcko commented at 8:06 am on October 24, 2023:
    In any case, I don’t think this is a blocker for rc1. It is not a regression and this diff can be added to rc2 trivially.

    vasild commented at 9:35 am on October 24, 2023:

    #27935 (review) still could call Ban() with an invalid address or subnet (default constructed is !IsValid()). I am not sure if we should be doing that from the tests:

    • No real, non-testing code should be doing this, but still some buggy code may do
    • It is good to exercise that and see that BanMan does not crash
    • BanMan removes invalid entries when re-reading the bans from the json file, so it is unrealistic to expect that the ban entries will be the same before and after read from the json file
    • Further, this looks like an inconsistency in BanMan that it accepts and invalid entry into its map via Ban(), but rejects invalid entries from the json file. Maybe BanMan should reject invalid entries also from the Ban() method.

    brunoerg commented at 10:04 am on October 24, 2023:
    I think the problem is the logic is all in the RPC (lookup calls and the vailidity check). We could move it to a function.

    vasild commented at 3:09 pm on November 3, 2023:

    This is still an issue in the latest version (8adbd44c9b). It seems unlikely that the fuzzer will generate a string that makes LookupHost() happy. I think it is best if this test keeps using ConsumeNetAddr() and ConsumeSubNet() but those functions should return valid/sensible values. Something like:

    0inline CSubNet ConsumeSubNet(FuzzedDataProvider& fuzzed_data_provider) noexcept
    1{
    2    const CNetAddr net_base = ConsumeNetAddr(fuzzed_data_provider, {NET_IPV4, NET_IPV6}, /*always_valid=*/true);
    3    const uint8_t cidr_max = net_base.IsIPv4() ? ADDR_IPV4_SIZE : ADDR_IPV6_SIZE;
    4    return {net_base, fuzzed_data_provider.ConsumeIntegralInRange<uint8_t>(0, cidr_max * 8)};
    5}
    

    maflcko commented at 3:18 pm on November 3, 2023:

    but those functions should return valid/sensible values.

    I think it is good to offer a path for the fuzz engine to produce an invalid value. Otherwise, it can miss bugs that only happen when an invalid value is passed.


    vasild commented at 5:01 pm on November 3, 2023:
    Yes, this was my doubt too. But then it is unrealistic to expect that banman ser/deser will produce identical result as the input. Another try - what about feeding it anything (including invalid stuff), but keeping track if we ever fed it an invalid address or subnet and if yes, then don’t do the assert at the end assert(banmap == banmap_read);?

    maflcko commented at 5:14 pm on November 3, 2023:

    Another try - what about feeding it anything (including invalid stuff), but keeping track if we ever fed it an invalid address or subnet and if yes, then don’t do the assert at the end assert(banmap == banmap_read);?

    sgtm


    brunoerg commented at 5:43 pm on November 3, 2023:

    Yes, this was my doubt too. But then it is unrealistic to expect that banman ser/deser will produce identical result as the input. Another try - what about feeding it anything (including invalid stuff), but keeping track if we ever fed it an invalid address or subnet and if yes, then don’t do the assert at the end assert(banmap == banmap_read);?

    sgtm as well. Can I change the approach so?

  41. maflcko commented at 9:30 am on October 23, 2023: member

    No opinion on the changes, as I am not familiar with low level net stuff.

    I left a question about the approach.

  42. fanquake added this to the milestone 26.0 on Oct 23, 2023
  43. dergoegge commented at 9:12 am on October 24, 2023: member
    With this PR rebased on master the fuzz target fails on ZmNmYTo6MVwzMyo6XG8x//+SfgQAsGAAAFtbAFs6AAAAgG6yQeqTIA== (took 1000+ CPU hours). Might be unrelated or the same issue, haven’t checked.
  44. maflcko commented at 9:16 am on October 24, 2023: member
    Does it fail on the same assertion?
  45. dergoegge commented at 9:18 am on October 24, 2023: member
    Yes
  46. vasild commented at 7:45 am on October 25, 2023: contributor

    I guess there are two possible approaches:

    1. Shorter and less invasive/risky - change the fuzz test to not ban invalid subnets. Calling LookupSubNet() should not be necessary and I guess IsValid() should suffice? Is there a case where a subnet has IsValid() == true but LookupSubNet() fails on it? Also, IPv6 subnets that start with fc while NET_CJDNS is reachable are “invalid”.

    2. Change BanMan::Ban() to not add such invalid networks to its internal map and change it to return bool, false meaning “I did not ban that” and true meaning “A ban was added”.

    Maybe do 1. for 26.0 and 2. for 27.0?

  47. DrahtBot added the label CI failed on Oct 28, 2023
  48. maflcko commented at 1:51 pm on October 30, 2023: member
    Looks like this can’t be merged/backported as-is either way, due to the silent conflict caused by the rafactor (7be62df80ff4d1339c039c6589232e50274b4b1a)?
  49. brunoerg commented at 1:53 pm on October 30, 2023: contributor

    Looks like this can’t be merged/backported as-is either way, due to the silent conflict caused by the rafactor (https://github.com/bitcoin/bitcoin/commit/7be62df80ff4d1339c039c6589232e50274b4b1a)?

    Gonna check it

  50. vasild commented at 9:15 am on October 31, 2023: contributor

    One more consideration wrt to changing Ban() to refuse invalid stuff (2. in above #27935 (comment)) - it will not match later anyway due to:

    https://github.com/bitcoin/bitcoin/blob/4458ae811a264968c2a7ea4bb7050eed492a7e36/src/netaddress.cpp#L1005-L1008

  51. brunoerg force-pushed on Oct 31, 2023
  52. brunoerg commented at 11:54 am on October 31, 2023: contributor
    Rebased and changed the approach in fuzz to call Ban only with valid net address/subnet.
  53. DrahtBot removed the label CI failed on Oct 31, 2023
  54. maflcko commented at 8:21 am on November 8, 2023: member
    Are you still working on this?
  55. brunoerg commented at 9:23 am on November 8, 2023: contributor

    Are you still working on this?

    Yes, will push an update soon

  56. brunoerg force-pushed on Nov 8, 2023
  57. brunoerg commented at 7:40 pm on November 8, 2023: contributor

    Force-pushed:

    • Added a check to control whether we called Ban with an invalid subnet/netaddr. If so, we don’t compare the banmaps.
    • I did some experiments and I decided to keep the lookup functions. Without it, I still get discrepances even avoiding the comparison when we call Ban with an invalid subnet/netaddr.
  58. maflcko commented at 10:45 am on November 9, 2023: member

    I did some experiments and I decided to keep the lookup functions. Without it, I still get discrepances even avoiding the comparison when we call Ban with an invalid subnet/netaddr.

    Would be good to explain this a bit more, to make sure this is not a bug.

  59. brunoerg commented at 11:56 am on November 9, 2023: contributor

    Would be good to explain this a bit more, to make sure this is not a bug.

    I think fuzz can generate subnets with a zone identifier, like: “676:c962:7962:b787:b392:fed8:7058:c500%2038004089/121”, which is a valid one. However, the lookup call might change the zone identifier. In my machine (macOS), “676:c962:7962:b787:b392:fed8:7058:c500%2038004089/121” becomes “676:c962:7962:b787:b392:fed8:7058:c500%31097/121” after lookup and it makes the assertion fails.

  60. maflcko commented at 12:12 pm on November 9, 2023: member
    Ah ok. And about the efficiency, is it possible to keep the fuzz input format unchanged instead of using a string representation (Maybe via a string-lookup roundtrip)? I wonder if that’d be more efficient.
  61. fuzz: call lookup functions before calling `Ban`
    Also, compare banmaps only if there are no invalid
    entries.
    f9b286353f
  62. ci: remove "--exclude banman" for fuzzing in mac fca0a8938e
  63. brunoerg force-pushed on Nov 9, 2023
  64. brunoerg commented at 3:17 pm on November 9, 2023: contributor

    Ah ok. And about the efficiency, is it possible to keep the fuzz input format unchanged instead of using a string representation (Maybe via a string-lookup roundtrip)? I wonder if that’d be more efficient.

    Yes, force-pushed addressing it. Also, I think this way our seeds remain great.

  65. maflcko commented at 5:58 pm on November 9, 2023: member

    lgtm ACK fca0a8938e34cb4f6c400e1d1d0be02f027d80c5

    I did not check the performance difference against 7e644308805229ab64455c01a22531756644fe69

  66. DrahtBot requested review from dergoegge on Nov 9, 2023
  67. DrahtBot requested review from jonatack on Nov 9, 2023
  68. dergoegge approved
  69. dergoegge commented at 10:23 am on November 13, 2023: member
    ACK fca0a8938e34cb4f6c400e1d1d0be02f027d80c5
  70. fanquake merged this on Nov 13, 2023
  71. fanquake closed this on Nov 13, 2023

  72. vasild commented at 1:44 pm on November 13, 2023: contributor
    ACK fca0a8938e34cb4f6c400e1d1d0be02f027d80c5
  73. luke-jr referenced this in commit d502efe75f on Mar 8, 2024
  74. luke-jr referenced this in commit 7909c83921 on Mar 8, 2024
  75. luke-jr referenced this in commit f76c89c7ef on Mar 13, 2024
  76. bitcoin locked this on Nov 12, 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: 2024-11-23 15:12 UTC

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