tests: improves tapscript unit tests #31640

pull EthanHeilman wants to merge 2 commits into bitcoin:master from EthanHeilman:taptest changing 2 files +88 −2
  1. EthanHeilman commented at 1:14 am on January 11, 2025: contributor

    This commit creates new test utilities for future Taproot script tests within script_tests.json. The key features of this commit are the addition of three new tags: #SCRIPT#, #CONTROLBLOCK#, and #TAPROOTOUTPUT#. These tags streamline the test creation process by eliminating the need to manually generate these components outside the test suite.

    • #SCRIPT#: Parses Tapscript and outputs a byte string of opcodes.
    • #CONTROLBLOCK#: Automatically generates the control block for a given Taproot output.
    • #TAPROOTOUTPUT#: Generates the final Taproot scriptPubKey.

    This code was originally part of the OP_CAT PR #29247 but was pulled out into a separate PR to reduce the rebase treadmill for the OP_CAT PR.

    Additionally this PR adds a check to ensure that if the witness data can not be parsed as hex the test fails. Prior to this PR, the test code would fail silently and set the values it couldn’t parse as empty stack elements. This fix was suggested by @instagibbs.

    Rationale

    While writing JSON script tests (script_tests.json) for #29247 we ran into the following problem. The JSON script tests are simple and easy to write for pre-Tapscript scripts, but adding or changing a Tapscript test requires substantial work per test. Consider the following pre-tapscript test:

    0["'aa' 'bb'", "CAT 0x4c 0x02 0xaabb EQUAL", "P2SH,STRICTENC", "DISABLED_OPCODE", "CAT disabled"]
    

    whereas a Tapscript test for the same script (annotated with comments for better readability) would look like:

     0[
     1    [
     2        "aa",
     3        "bb",
     4        "7e4c02aabb87", // output script
     5        "c0d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d", // control block
     6        0.00000001
     7    ],
     8    "",
     9    "0x51 0x20 0x15048ed3a65748549c27b671936987093cf73a4c9cb18522a74fb9553060ca99", // Tapscript output
    10    "P2SH,WITNESS,TAPROOT",
    11    "OK",
    12    "TAPSCRIPT CATs aa and bb together and checks if EQUAL to aabb"
    13]
    

    Computing the Tapscript output, such as 0x51 0x20 0x15048ed3a65748549c27b671936987093cf73a4c9cb18522a74fb9553060ca99, requires writing custom code and running it for each test. The same is true for the Tapscript control block, such as c0d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d. If a test is changed or updated new outputs and control blocks must be computed. The complexity of doing this is likely the reason that no one has added any Tapscript tests to JSON script tests until this PR.

    In this PR we address this issue by adding the following improvements to JSON script tests:

    Adding simple macros ("#SCRIPT# and #CONTROLBLOCK#) that allow the script test parser to automatically generate and inject a valid Tapscript output and control block to be computed automatically from the JSON script. Allowing Tapscript scripts to use the human readable strings like pre-script scripts by marking the location of the script in the witness stack using #SCRIPT#. This transforms the unreadable script 7e4c02aabb87 into #SCRIPT# CAT 0x4c 0x02 0xaabb EQUAL. This results in the following JSON script test which is far easier to write and easier to read.

     0[
     1    [
     2        "aa",
     3        "bb",
     4        "#SCRIPT# CAT",
     5        "#CONTROLBLOCK#",
     6        0.00000001
     7    ],
     8    "",
     9    "0x51 0x20 #TAPROOTOUTPUT#",
    10    "P2SH,WITNESS,TAPROOT,OP_CAT",
    11    "OK",
    12    "TAPSCRIPT Test of OP_CAT flag by calling CAT on two elements. TAPSCRIPT_OP_CAT flag is set so CAT is executed."
    13],
    
  2. DrahtBot commented at 1:14 am on January 11, 2025: contributor

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

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/31640.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK dergoegge, sipa
    Stale ACK 0xBEEFCAF3, janb84

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

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #29247 (CAT in Tapscript (BIP-347) by 0xBEEFCAF3)

    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. DrahtBot added the label Tests on Jan 11, 2025
  4. EthanHeilman force-pushed on Jan 11, 2025
  5. DrahtBot commented at 1:17 am on January 11, 2025: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/35460490112

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  6. DrahtBot added the label CI failed on Jan 11, 2025
  7. EthanHeilman force-pushed on Jan 11, 2025
  8. EthanHeilman force-pushed on Jan 11, 2025
  9. EthanHeilman force-pushed on Jan 11, 2025
  10. EthanHeilman force-pushed on Jan 11, 2025
  11. EthanHeilman force-pushed on Jan 11, 2025
  12. EthanHeilman force-pushed on Jan 11, 2025
  13. DrahtBot removed the label CI failed on Jan 11, 2025
  14. instagibbs commented at 3:18 pm on January 11, 2025: member
    is this in draft for a reason?
  15. EthanHeilman commented at 2:31 pm on January 14, 2025: contributor

    is this in draft for a reason? @instagibbs Yes, my todos to make this not a draft are:

    [X] - Better PR description [X] - A few extra tapscript tests (it was most cat tapscript tests before) [X] - Doublechecking my code to make sure the rebase didn’t break anything

    I got most of this done over the weekend, I just haven’t pushed it yet. Will likely push it tonight, Wednesday.

    Edit just pushed. Not longer a draft.

  16. EthanHeilman force-pushed on Jan 15, 2025
  17. EthanHeilman force-pushed on Jan 15, 2025
  18. DrahtBot added the label CI failed on Jan 15, 2025
  19. DrahtBot commented at 0:40 am on January 15, 2025: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/35623022004

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  20. EthanHeilman force-pushed on Jan 15, 2025
  21. EthanHeilman marked this as ready for review on Jan 15, 2025
  22. EthanHeilman force-pushed on Jan 15, 2025
  23. EthanHeilman force-pushed on Jan 15, 2025
  24. in src/test/data/script_tests.json:2626 in e3343db8b2 outdated
    2621+    ],
    2622+    "",
    2623+    "0x51 0x20 #TAPROOTOUTPUT#",
    2624+    "P2SH,WITNESS,TAPROOT",
    2625+    "OK",
    2626+    "TAPSCRIPT Tests testing tapscript with many different op codes including ALTSTACK interactions. "
    


    0xBEEFCAF3 commented at 1:10 am on January 15, 2025:
    Super nit: empty space on this line. Not sure if the linter even picks up on this

    EthanHeilman commented at 1:12 am on January 15, 2025:
    Fixed! Thanks
  25. EthanHeilman force-pushed on Jan 15, 2025
  26. EthanHeilman requested review from 0xBEEFCAF3 on Jan 15, 2025
  27. DrahtBot removed the label CI failed on Jan 15, 2025
  28. in src/test/script_tests.cpp:964 in 1bd2af1de6 outdated
    957@@ -940,7 +958,14 @@ BOOST_AUTO_TEST_CASE(script_json_test)
    958         std::string scriptSigString = test[pos++].get_str();
    959         CScript scriptSig = ParseScript(scriptSigString);
    960         std::string scriptPubKeyString = test[pos++].get_str();
    961-        CScript scriptPubKey = ParseScript(scriptPubKeyString);
    962+        CScript scriptPubKey;
    963+        // If requested, auto-generate the taproot output
    964+        if (strcmp(scriptPubKeyString.c_str(), "0x51 0x20 #TAPROOTOUTPUT#")== 0) {
    965+            BOOST_CHECK_MESSAGE(taprootBuilder.IsComplete(), "Failed to autogenerate Tapscript Script PubKey");
    


    0xBEEFCAF3 commented at 3:14 pm on January 20, 2025:
    Nit: I would use “taproot output key” here instead of “Tapscript Script Pubkey” per BIP341

    EthanHeilman commented at 3:51 pm on January 20, 2025:
    Fixed!
  29. 0xBEEFCAF3 approved
  30. 0xBEEFCAF3 commented at 3:29 pm on January 20, 2025: none
    tACK 1bd2af1de6551d67c1c363b4cd04d4973e3650bf Tested by rebasing the new bip347 tapscript unit tests on top of these changes.
  31. dergoegge commented at 10:38 am on February 4, 2025: member

    Concept ACK

    I would suggest that you squash your commits (as you are fixing up the first commit in the second one).

    Just as a note, there’s also test/functional/feature_taproot.py which produces taproot unit tests. I’m not sure if those were meant to be extended in the case of Tapscript updates but that seems a bit daunting anyway compared to what you suggest here.

  32. EthanHeilman force-pushed on Feb 4, 2025
  33. EthanHeilman commented at 8:17 pm on February 4, 2025: contributor

    @dergoegge

    Squashed.

    Just as a note, there’s also test/functional/feature_taproot.py which produces taproot unit tests. I’m not sure if those were meant to be extended in the case of Tapscript updates but that seems a bit daunting anyway compared to what you suggest here.

    Yes, the idea here is to have the nice script unittest tests work for Tapscript. These tests are much more lightweight and are fast and easy to write.

  34. DrahtBot added the label CI failed on Feb 4, 2025
  35. DrahtBot removed the label CI failed on Feb 4, 2025
  36. fanquake requested review from instagibbs on Feb 25, 2025
  37. dergoegge commented at 3:15 pm on March 3, 2025: member
    I think this is a good addition to the unit test framework but I’d like to see some more tapscript specific test cases added in this PR as well (e.g. OP_SUCCESSx opcodes, schnorr sigs, no check multi sig, …).
  38. EthanHeilman commented at 5:43 pm on March 3, 2025: contributor

    @dergoegge I agree and I’d like to add those tests in a second PR if this PR is merged?

    I don’t have a lot of time this month and next to work on them and I worry about putting in more time for a PR that may not be merged.

  39. EthanHeilman commented at 2:59 pm on March 20, 2025: contributor
    @instagibbs Any changes you want me to make?
  40. instagibbs commented at 3:01 pm on March 20, 2025: member
    Let me take a deeper look
  41. EthanHeilman force-pushed on Mar 20, 2025
  42. EthanHeilman force-pushed on Mar 20, 2025
  43. EthanHeilman force-pushed on Mar 20, 2025
  44. DrahtBot added the label CI failed on Mar 20, 2025
  45. DrahtBot commented at 11:00 pm on March 20, 2025: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/39145193596

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  46. EthanHeilman force-pushed on Mar 20, 2025
  47. EthanHeilman force-pushed on Mar 20, 2025
  48. EthanHeilman force-pushed on Mar 20, 2025
  49. EthanHeilman force-pushed on Mar 21, 2025
  50. EthanHeilman force-pushed on Mar 21, 2025
  51. EthanHeilman force-pushed on Mar 21, 2025
  52. EthanHeilman force-pushed on Mar 21, 2025
  53. EthanHeilman force-pushed on Mar 21, 2025
  54. EthanHeilman force-pushed on Mar 21, 2025
  55. EthanHeilman force-pushed on Mar 21, 2025
  56. DrahtBot removed the label CI failed on Mar 21, 2025
  57. darosior commented at 1:28 pm on March 21, 2025: member

    The complexity of doing this is likely the reason that no one has added any Tapscript tests to JSON script tests until this PR.

    I think the reason no one has added more tests is because the Tapscript logic is already extensively covered through the JSON script unit tests (in additional to the extensive functional test). See this unit test (which works with this JSON file from the qa-assets repo): https://github.com/bitcoin/bitcoin/blob/804675930598ccd3102eab642d2b5aa3caa982d4/src/test/script_tests.cpp#L1605

  58. sipa commented at 2:11 pm on March 21, 2025: member

    I do understand the desire to have accessible simple unit test cases for tapscript, and more testing certainly doesn’t hurt. However, making things like this possible and easy to write was the goal of the taproot test framework, which has advantages like:

    • supporting combinations with non-trivial script trees
    • adding auto-generated signatures
    • integrating it all into transactions and blocks and running through the entire stack
    • having the ability to export the resulting cases to JSON to incorporate them in the static test cases (which also form the BIP342 test vectors).

    Efforts to expand script testing, especially when done in the context of proposed changes to the script language, really should not be restricted to simple cases.

    As I realize the framework is pretty daunting to start with, here is what the translation of your added cases here would be there:

     0diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py
     1index daf11a9ae6c..3c3ac37bd94 100755
     2--- a/test/functional/feature_taproot.py
     3+++ b/test/functional/feature_taproot.py
     4@@ -64,13 +64,19 @@ from test_framework.script import (
     5     OP_ENDIF,
     6     OP_EQUAL,
     7     OP_EQUALVERIFY,
     8+    OP_FROMALTSTACK,
     9     OP_IF,
    10+    OP_HASH256,
    11     OP_NOP,
    12     OP_NOT,
    13     OP_NOTIF,
    14     OP_PUSHDATA1,
    15     OP_RETURN,
    16+    OP_SIZE,
    17+    OP_SHA1,
    18+    OP_SHA256,
    19     OP_SWAP,
    20+    OP_TOALTSTACK,
    21     OP_VERIFY,
    22     SIGHASH_DEFAULT,
    23     SIGHASH_ALL,
    24@@ -1186,6 +1192,17 @@ def spenders_taproot_active():
    25     tap = taproot_construct(pubs[0], [("leaf", CScript([pubs[1], OP_CHECKSIG, pubs[1], OP_CHECKSIGADD, OP_2, OP_EQUAL])), zero_fn])
    26     add_spender(spenders, "case24765", tap=tap, leaf="leaf", inputs=[getter("sign"), getter("sign")], key=secs[1], no_fail=True)
    27 
    28+    # == PR 31640 replacement
    29+    scripts = [
    30+        ("manyops", CScript([OP_HASH256, OP_DUP, OP_SHA1, OP_DROP, OP_DUP, OP_TOALTSTACK, OP_HASH256, OP_DUP, OP_DROP, OP_TOALTSTACK, OP_FROMALTSTACK])),
    31+        ("ifcond", CScript([OP_IF, OP_SHA256, OP_ENDIF, OP_SIZE, OP_SWAP, OP_DROP, 32, OP_EQUAL])),
    32+        ("popif", CScript([OP_EQUAL, OP_IF, OP_DROP, OP_DROP, OP_ENDIF]))
    33+    ]
    34+    tap = taproot_construct(pubs[0], scripts)
    35+    add_spender(spenders, "pr31640/manyops", tap=tap, leaf="manyops", inputs=[b'\x00'], no_fail=True)
    36+    add_spender(spenders, "pr31640/ifcond", tap=tap, leaf="ifcond", inputs=[b'abcdef', b'\x01'], failure={"inputs": [b'abcdef', b'']}, **ERR_NO_SUCCESS)
    37+    add_spender(spenders, "pr31640/popif", tap=tap, leaf="popif", inputs=[b'\xaa', b'\xbb', b'\xcc'], no_fail=True)
    38+
    39     # == Legacy tests ==
    40 
    41     # Also add a few legacy spends into the mix, so that transactions which combine taproot and pre-taproot spends get tested too.
    

    I hope that makes the case for using the framework instead.

  59. EthanHeilman commented at 4:20 pm on March 21, 2025: contributor

    @sipa

    This PR doesn’t contain any improvements to the python functional tests because they are excellent and did not require modifications to write clear tapscript tests. This PR is not intended to replace more complex tests. It is only intended to make simple unit tests easy to write and read.

    In defense of simple unittests for tapscript:

    1. JSON script_test is the lowest effort way to add a test Adding a JSON script_test for everything but tapscript is trivially easy. Given human nature you’ll get more tests if adding a test takes 5 minutes with an 1 hour learning curve than if it takes 1 hour with 1 week learning curve. I want to make it as easy as possible for someone who encounters something unexpected in a tapscript to write a test for it.

    2. script_test is expansive As a consequence of JSON script_test being easy to add they cover many cases. We caught bugs early in development because there was an existing simple test that immediately failed. There was one case in particular with counter-intuitive (to me) script interpreter behavior that only script test caught. (behavior around false OP_IF branch data pushes)

    3. Fail faster Failing in unittests rather failing in functional tests results in faster cycles.

    4. Easy to debug It is easier to debug C++ code from inside a C++ unit tests rather than python functional tests. I often use script_tests to explore script interpreter behavior. I replace script_test.json with the script I want to test, set a breakpoint and run.

    I’ve found this workflow of debugging with script_tests valuable to me that I plan to extend it and provide a way to specify a script test at runtime via an argument rather than compile time via the json file. I know this is possible with some of the fuzzing harnesses but it isn’t pleasant. The script_test format is easier to work with for everything but tapscript and with this PR tapscript becomes easy to work with.

    Is there a way to do script flag tests in the functional tests?

  60. EthanHeilman commented at 4:20 pm on March 21, 2025: contributor

    @darosior

    The static test cases are valuable, but they are multistep process to create and are hard to read. Imagine you write some code and get a test failure.

    Would you rather the test failure be:

    0{"tx": "f705d6e8019870958e85d1d8f94aa6d74746ba974db0f5ccae49a49b32dcada4e19de4eb5ecb00000000925977cc01f9875c000000000016001431d2b00cd4687ceb34008d9894de84062def14aa05406346", "prevouts": ["b4eae1010000000022512039f7e9232896f8100485e38afa652044f855e734a13b840a3f220cbd5d911ad5"], "index": 0, "success": {"scriptSig": "", "witness": ["25e45bd4d4b8fcd5933861355a2d376aad8daf1af1588e5fb6dfcea22d0d809acda6fadca11e97f5b5c85af99df27cb24fa69b08fa6c790234cdc671d3af5a7302"]}, "failure": {"scriptSig": "", "witness": [""]}, "flags": "P2SH,DERSIG,CHECKLOCKTIMEVERIFY,CHECKSEQUENCEVERIFY,WITNESS,NULLDUMMY,TAPROOT", "final": true, "comment": "siglen/empty_keypath"},
    

    or that and this more readable unit test

     0[
     1    [
     2        "abcdef",
     3        "#SCRIPT# 1 IF SHA256 ENDIF SIZE SWAP DROP 32 EQUAL",
     4        "#CONTROLBLOCK#",
     5        0.00000001
     6    ],
     7    "",
     8    "0x51 0x20 #TAPROOTOUTPUT#",
     9    "P2SH,WITNESS,TAPROOT",
    10    "OK",
    11    "TAPSCRIPT Test IF conditional when true"
    12],
    
  61. EthanHeilman force-pushed on Mar 21, 2025
  62. instagibbs commented at 4:33 pm on March 21, 2025: member

    Would you rather the test failure be:

    If you “walk” backwards to the functional test via the comment field it’s ok, but I agree I would rather the tests cases be declared in the functional test rather than the dumped qa-assets version which lacks the context.

    With regards to debugging, I found both this and #32114 similar in pain to review/fix, but I admit I’ve gotten used to gdb-attaching to bitcoind for functional tests regardless.

  63. sipa commented at 7:53 pm on March 21, 2025: member

    @EthanHeilman Those are convincing arguments for adding simple tapscript unit tests, agreed.

    Is there a way to do script flag tests in the functional tests?

    Only to the extent that it can be done by demonstrating differing behavior pre/post softfork activation, not per individual script validation flag.

    The static test cases are valuable, but they are multistep process to create and are hard to read. Imagine you write some code and get a test failure.

    To be fair, I don’t think the static tests cases are useful during iterative development; you’d just run feature_taproot.py directly instead, which regenerates everything, runs it on the fly against bitcoind, and reports any failure (which you’ll need to retrieve through its comment field). It’s certainly slower than just running a quick unit test, but not nearly as involved as updating the static test file.

    The static test file is there to make sure there is a well-defined unchangeable set of vectors, which could even be ran in alternative implementations.

  64. instagibbs approved
  65. instagibbs commented at 2:23 pm on March 24, 2025: member

    LGTM dfea1fae92a1fd8ba0bbf5d52a37baffa28f9f8f

    I think having “fail-fast”, developer-friendly tests is a fine motivation. And nothing stops me/you from porting novel things to the functional framework for additional coverage.

    The additional sanity check added in first commit also helps protect against test regressions in the future.

    Hand-checked the scripts, seems correct.

  66. in src/test/script_tests.cpp:948 in dfea1fae92 outdated
    944+                    witness.stack.push_back(*(controlblocks.begin()));
    945+                } else {
    946+                    const auto witness_value{TryParseHex<unsigned char>(element)};
    947+                    if (!witness_value.has_value()) {
    948+                        BOOST_ERROR("Bad witness in test: " << strTest << " witness is not hex: " << element);
    949+                        assert(witness_value.has_value());
    


    janb84 commented at 7:15 pm on March 27, 2025:

    Found this construction bid odd, asserting something that is known to fail. If the assertion is removed the unit test fails more gracefully, from 234 failures reported to 2 failures reported (I made one witness non hex). Is this approach deliberate?


    EthanHeilman commented at 10:21 pm on March 27, 2025:

    This approach is deliberate. My thinking here was that this wasn’t a test failure, i.e. functionality isn’t behaving as expected, but rather a failure in the tests themselves.

    That said, as you have shown, the boost error alone is more helpful for finding the fault. I’ll remove it. Good find

  67. janb84 commented at 8:47 am on March 28, 2025: contributor

    ACK b544d1c

    • Code review ✅
    • Tested ✅
  68. DrahtBot requested review from dergoegge on Mar 28, 2025
  69. DrahtBot requested review from 0xBEEFCAF3 on Mar 28, 2025
  70. DrahtBot requested review from sipa on Mar 28, 2025
  71. instagibbs commented at 1:17 pm on March 28, 2025: member
    could you squash commits?
  72. test: Ensures test fails if witness is not hex
    This commit ensures that we do not fail silently when the test script encounters a witness string in the JSON test data that can not be parsed as hex.
    3e167085ba
  73. test: improves tapscript unit tests
    This commit creates new test utilities for future Taproot script
    tests within script_tests.json. The key features of this commit are the
    addition of three new tags: `#SCRIPT#`, `#CONTROLBLOCK#`, and
    `#TAPROOTOUTPUT#`. These tags streamline the test creation process by
    eliminating the need to manually generate these components outside the
    test suite.
    
    * `#SCRIPT#`: Parses Tapscript and outputs a byte string of opcodes.
    * `#CONTROLBLOCK#`: Automatically generates the control block for a given
    Taproot output.
    * `#TAPROOTOUTPUT#`: Generates the final Taproot scriptPubKey.
    
    Update src/test/script_tests.cpp
    
    Co-authored-by: Jan B <608446+janb84@users.noreply.github.com>
    62b6071fd6
  74. EthanHeilman force-pushed on Mar 29, 2025
  75. EthanHeilman commented at 7:21 pm on March 30, 2025: contributor

    could you squash commits? @instagibbs Squashed


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: 2025-03-31 09:12 UTC

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