wallet, rpc: FundTransaction refactor #28560

pull josibake wants to merge 6 commits into bitcoin:master from josibake:fundtransaction-sffo-crecipient-refactor changing 9 files +216 −94
  1. josibake commented at 4:50 pm on October 2, 2023: member

    Motivation

    The primary motivation for this PR is to enable FundTransaction to take a vector of CRecipient objects to allow passing BIP352 silent payment addresses to RPCs that use FundTransaction (e.g. send, walletcreatefundedpsbt). To do that, SFFO logic needs to be moved out of FundTransaction so the CRecipient objects with the correct SFFO information can be created and then passed to FundTransaction.

    As a secondary motivation, this PR moves the SFFO stuff closer to the caller, making the code cleaner and easier to understand. This is done by having a single function which parses RPC inputs for SFFO and consistently using the set<int> method for communicating SFFO.

    I’m also not convinced we need to pass a full CMutableTx object to FundTransaction, but I’m leaving that for a follow-up PR/discussion, as its not a blocker for silent payments.

  2. DrahtBot commented at 4:50 pm on October 2, 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 S3RK, achow101
    Concept ACK theStack

    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:

    • #bitcoin-core/gui/733 (Deniability - a tool to automatically improve coin ownership privacy by denavila)
    • #29264 (Add max_tx_weight to transaction funding options by instagibbs)
    • #28944 (wallet, rpc: add anti-fee-sniping to send and sendall by ishaanam)
    • #27792 (wallet: Deniability API (Unilateral Transaction Meta-Privacy) by denavila)

    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. josibake marked this as a draft on Oct 2, 2023
  4. DrahtBot added the label Needs rebase on Oct 16, 2023
  5. josibake force-pushed on Dec 11, 2023
  6. josibake force-pushed on Dec 11, 2023
  7. josibake marked this as ready for review on Dec 11, 2023
  8. DrahtBot removed the label Needs rebase on Dec 11, 2023
  9. josibake force-pushed on Dec 12, 2023
  10. josibake force-pushed on Dec 12, 2023
  11. josibake force-pushed on Dec 12, 2023
  12. in src/wallet/rpc/spend.cpp:76 in f0af6e9a0f outdated
    72@@ -73,6 +73,38 @@ std::set<int> InterpretSubtractFeeFromOutputInstructions(const UniValue& subtrac
    73     return set_sffo;
    74 }
    75 
    76+std::vector<CRecipient> ParseOutputs(const UniValue& outputs_in, const UniValue& options)
    


    S3RK commented at 8:19 am on December 20, 2023:
    Let’s not pass all the options as we need only one of them.

    josibake commented at 3:20 pm on December 20, 2023:
    We are using either subtract_fee_from_outputs or subtractFeeFromOutputs, depending on which one is present. I think handling that logic in the ParseOutputs function is better. Since the options object is passed by reference there isn’t any performance difference in passing a reference to the entire options object vs passing a single key.
  13. in src/wallet/rpc/spend.cpp:37 in 3dfcb817d8 outdated
    38-        if (destinations.count(dest)) {
    39-            throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, duplicated address: ") + address);
    40+        CTxDestination dest;
    41+        CAmount amount{0};
    42+        if (address == "data") {
    43+            if (has_data) {
    


    S3RK commented at 8:23 am on December 20, 2023:

    has_data never set to true

    Also no tests fail, so it seems there is no test coverage for that


    josibake commented at 3:20 pm on December 20, 2023:
    Good catch!
  14. in src/wallet/spend.h:227 in 3dfcb817d8 outdated
    223@@ -224,7 +224,7 @@ util::Result<CreatedTransactionResult> CreateTransaction(CWallet& wallet, const
    224  * Insert additional inputs into the transaction by
    225  * calling CreateTransaction();
    226  */
    227-util::Result<CreatedTransactionResult> FundTransaction(CWallet& wallet, const CMutableTransaction& tx, std::optional<unsigned int> change_pos, bool lockUnspents, const std::set<int>& setSubtractFeeFromOutputs, CCoinControl);
    228+util::Result<CreatedTransactionResult> FundTransaction(CWallet& wallet, const CMutableTransaction& tx, const std::vector<CRecipient>& recipients, std::optional<unsigned int> change_pos, bool lockUnspents, CCoinControl);
    


    S3RK commented at 8:38 am on December 20, 2023:

    Not a huge fan of this change. Now if we pass tx.vout they’ll be ignored.

    I’d prefer if we pass txin and recipients and don’t pass tx at all. That would be a clearer interface in my opinion.


    josibake commented at 3:34 pm on December 20, 2023:
    I agree, see my comment in the description. Ultimately, I think FundTransaction should take a set of inputs and a set of outputs and return a CreatedTransactionResult. #25273 and this PR get us incrementally closer to that, but I don’t want to fully tackle that in this PR as it’s a bit more complicated than it seems, and the primary goal of this PR is to create the CRecipients objects in a standard way across all the RPC calls and pass them to FundTransaction

    achow101 commented at 4:54 pm on January 5, 2024:
    I think it would be useful to assert that tx.vout.empty() somewhere to make it clear that it is not used. Perhaps this function can do tx.vout.clear(), and the FundTransaction called by it can do the check. We should make sure that any changes between now and the future PR do not accidentally use tx.vout.

    josibake commented at 5:31 pm on January 5, 2024:
    Good point; I’ll add a check and a comment.

    josibake commented at 6:11 pm on January 5, 2024:
    Rather than clearing tx.vout in the outer RPC function call, I am clearing tx.vout at the call sites of FundTransaction. Otherwise, I would have needed to make tx non-const, which seems risky since we are still using it to pass tx.vins
  15. in src/wallet/rpc/spend.cpp:493 in 3dfcb817d8 outdated
    542@@ -532,17 +543,14 @@ static std::vector<RPCArg> FundTxDoc(bool solving_data = true)
    543     return args;
    544 }
    545 
    546-CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransaction& tx, const UniValue& options, CCoinControl& coinControl, bool override_min_fee)
    547+CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransaction& tx, const std::vector<CRecipient>& recipients, const UniValue& options, CCoinControl& coinControl, bool override_min_fee)
    


    S3RK commented at 8:41 am on December 20, 2023:
    Same comment as for other FundTransaction. I think we should get rid of ambiguity in input parameters and don’t pass tx. In this case it’s even more confusing, because tx.vout is actually still checked.

    josibake commented at 3:43 pm on December 20, 2023:
    Also a good catch, we should be using the recipients vector to get the size here.
  16. S3RK commented at 8:42 am on December 20, 2023: contributor
    I’m still trying to fully grasp the existing code (which is a big mess), so bear with me if I don’t make sense. Left some comment/questions based on my current understanding.
  17. josibake force-pushed on Dec 20, 2023
  18. josibake commented at 4:06 pm on December 20, 2023: member
    Fixed the has_data logic and replaced all references to tx.vout with recipients in FundTransaction, per feedback from @S3RK
  19. josibake force-pushed on Dec 22, 2023
  20. josibake force-pushed on Dec 22, 2023
  21. DrahtBot added the label CI failed on Dec 22, 2023
  22. josibake force-pushed on Dec 22, 2023
  23. DrahtBot removed the label CI failed on Dec 22, 2023
  24. in test/functional/wallet_fundrawtransaction.py:167 in a9d67ad96e outdated
    162+        self.generate(self.nodes[0], 1)
    163+
    164+        address = self.nodes[0].getnewaddress("bech32")
    165+        tx = CTransaction()
    166+        tx.vin = []
    167+        tx.vout = [CTxOut(1 * COIN, bytearray(address_to_scriptpubkey(address))) for _ in range(2)]
    


    achow101 commented at 5:17 pm on January 3, 2024:

    In a9d67ad96e5f4d6dca64df4ac85f0434e17248ab “test: add tests for fundrawtx and sendmany rpcs”

    nit: More pythonic to write:

    0        tx.vout = [CTxOut(1 * COIN, bytearray(address_to_scriptpubkey(address)))] * 2
    

    josibake commented at 2:05 pm on January 4, 2024:
    noted! will update
  25. in test/functional/wallet_sendmany.py:9 in a9d67ad96e outdated
    0@@ -0,0 +1,43 @@
    1+#!/usr/bin/env python3
    2+# Copyright (c) 2022 The Bitcoin Core developers
    3+# Distributed under the MIT software license, see the accompanying
    4+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
    5+"""Test the sendmany RPC command."""
    6+
    7+from test_framework.test_framework import BitcoinTestFramework
    8+
    9+class SendmanyTest(BitcoinTestFramework):
    


    achow101 commented at 5:22 pm on January 3, 2024:

    In a9d67ad96e5f4d6dca64df4ac85f0434e17248ab “test: add tests for fundrawtx and sendmany rpcs”

    There are existing tests for sendmany in wallet_basic.py. I think these tests could be moved there as well.


    josibake commented at 2:09 pm on January 4, 2024:
    I initially started putting these tests in wallet_basic.py, but I’d prefer if we have RPC-specific test files and move all of the sendmany specific tests into the new wallet_sendmany.py file in a follow-up PR.
  26. in src/wallet/rpc/spend.cpp:32 in 589201443a outdated
    52-
    53-        CRecipient recipient = {dest, amount, subtract_fee};
    54+    std::vector<CRecipient> recipients;
    55+    int idx{0};
    56+    for (const auto& [destination, amount] : outputs) {
    57+        CRecipient recipient = {destination, amount, subtract_fee_outputs.count(idx) == 1};
    


    aureleoules commented at 4:21 pm on January 4, 2024:
    0        CRecipient recipient = {destination, amount, subtract_fee_outputs.contains(idx)};
    

    josibake commented at 5:46 pm on January 5, 2024:
    nice! TIL c++20 introduced contains for sets :heart:
  27. in src/wallet/rpc/spend.cpp:91 in 589201443a outdated
    86+    }
    87+    if (options.exists("subtractFeeFromOutputs") || options.exists("subtract_fee_from_outputs") ) {
    88+        subtract_fee_outputs = (options.exists("subtract_fee_from_outputs") ? options["subtract_fee_from_outputs"] : options["subtractFeeFromOutputs"]).get_array();
    89+        for (const auto& sffo : subtract_fee_outputs.getValues()) {
    90+            pos = sffo.getInt<int>();
    91+            if (sffo_set.count(pos))
    


    aureleoules commented at 4:21 pm on January 4, 2024:
    0            if (sffo_set.contains(pos))
    
  28. in src/wallet/rpc/spend.cpp:721 in 589201443a outdated
    717@@ -703,21 +718,10 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
    718         }
    719     }
    720 
    721-    if (tx.vout.size() == 0)
    722+    if (recipients.size() == 0)
    


    aureleoules commented at 4:22 pm on January 4, 2024:
    0    if (recipients.empty())
    
  29. aureleoules commented at 4:24 pm on January 4, 2024: member
    A few nits
  30. josibake force-pushed on Jan 6, 2024
  31. josibake force-pushed on Jan 6, 2024
  32. josibake commented at 6:45 pm on January 6, 2024: member

    git range-diff 5892014...909c6bb

    Addresses review comments from @aureleoules and @achow101 , most notable being now clearing tx.vout and asserting that it is empty inside both FundTransaction functions. This ensures this is no longer used to pass outputs to FundTransaction, now and in future code.

  33. in src/rpc/rawtransaction_util.cpp:107 in 8ab3da31c5 outdated
    107     std::set<CTxDestination> destinations;
    108+    std::vector<std::pair<CTxDestination, CAmount>> parsed_outputs;
    109     bool has_data{false};
    110-
    111+    CTxDestination destination;
    112+    CAmount amount;
    


    achow101 commented at 9:37 pm on January 8, 2024:

    In 8ab3da31c56d2cc05c4273ec0833109be106ed2b “refactor: move parsing to new function”

    I would that these two variables be declared inside of the loop just to avoid things leaking between iterations. I don’t see why they need to be outside of the loop.


    josibake commented at 7:58 pm on January 9, 2024:
    Fixed
  34. in src/wallet/rpc/spend.cpp:34 in d8850285d9 outdated
    54-        CRecipient recipient = {dest, amount, subtract_fee};
    55+    int idx{0};
    56+    for (const auto& [destination, amount] : outputs) {
    57+        CRecipient recipient = {destination, amount, subtract_fee_outputs.contains(idx)};
    58         recipients.push_back(recipient);
    59+        idx++;
    


    achow101 commented at 9:44 pm on January 8, 2024:

    In d8850285d9ebfbff77b6f04f80f1682a15c8783a “refactor: simplify CreateRecipients

    Can be simplified with just a normal for loop:

    0    for (size_t i = 0; i < outputs.size(); ++i) {
    1        const auto& [destination, amount] = outputs.at(i);
    2        CRecipient recipient = {destination, amount, subtract_fee_outputs.contains(i)};
    3        recipients.push_back(recipient);
    

    josibake commented at 7:58 pm on January 9, 2024:
    Done
  35. in src/wallet/rpc/spend.cpp:66 in d8850285d9 outdated
    57@@ -78,6 +58,36 @@ static void InterpretFeeEstimationInstructions(const UniValue& conf_target, cons
    58     }
    59 }
    60 
    61+std::set<int> InterpretSubtractFeeFromOutputInstructions(const UniValue& options, const std::vector<std::string>& destinations)
    62+{
    63+    int pos{0};
    64+    std::set<int> sffo_set;
    65+    UniValue subtract_fee_outputs(UniValue::VARR);
    66+    if (options.exists("subtractfeefromamount") || options.exists("subtractfeefrom")) {
    


    achow101 commented at 9:49 pm on January 8, 2024:

    In d8850285d9ebfbff77b6f04f80f1682a15c8783a “refactor: simplify CreateRecipients

    Using a UniValue in this function seems kinda clunky. I would much rather than the parsing be done by the RPCs that need to do the parsing and this function could just take a std::vector<std::string> sffo_addrs or something like that. None of these options seems to exist in other RPCs.

    Since all of the different ways that sffo is set by the user, this could do type interpretation to figure it out. If it’s a bool, and there’s only one destination, then that is the one to sffo, and the only way that happens is with sendtoaddress. If it’s an array of strings, then it’s sendmany and we do that one. And if it’s an array of ints, it’s the fundraw/send/walletcreatefunded type of sffo.


    josibake commented at 5:34 pm on January 9, 2024:

    Since all of the different ways that sffo is set by the user, this could do type interpretation to figure it out.

    That makes sense, what do you think about doing the type interpretation on the UniValue passed in, instead of relying on the key names? I’d rather do that than try to have this function take a std::variant or something like that


    achow101 commented at 6:19 pm on January 9, 2024:
    I think UniValue type interpretation would be fine.

    josibake commented at 7:57 pm on January 9, 2024:
    Done
  36. in src/wallet/rpc/spend.cpp:307 in d8850285d9 outdated
    308-        subtractFeeFromAmount.push_back(address);
    309-    }
    310-
    311-    std::vector<CRecipient> recipients = CreateRecipients(address_amounts, subtractFeeFromAmount);
    312+    UniValue subtractFeeFromAmount(UniValue::VOBJ);
    313+    subtractFeeFromAmount.pushKV("subtractfeefromamount", request.params[4]);
    


    achow101 commented at 9:52 pm on January 8, 2024:

    In d8850285d9ebfbff77b6f04f80f1682a15c8783a “refactor: simplify CreateRecipients

    Packing the sffo info into a UniValue just to be parsed again seems to be really unergonomic.


    josibake commented at 7:57 pm on January 9, 2024:
    Fixed
  37. josibake force-pushed on Jan 9, 2024
  38. josibake force-pushed on Jan 9, 2024
  39. josibake commented at 7:57 pm on January 9, 2024: member

    Updated https://github.com/bitcoin/bitcoin/commit/909c6bb1dbc6f60c5ad3200d83f3fce98f4c3706 -> https://github.com/bitcoin/bitcoin/commit/c856a1093009c6098c7c9fed5cc3f03fd0c0cf5b (compare)

    • Moved CTxDestination, Amount into loop
    • Simplified the loop inside CreateRecipients
    • Refactored InterpretSubtractFeeFromAmountInstructions to infer parsing logic based on the instruction type (int, bool, str)

    Thanks for the suggestion @achow101 , it looks a lot cleaner now.

  40. in src/wallet/rpc/spend.cpp:60 in 0b0276bb00 outdated
    56@@ -78,6 +57,27 @@ static void InterpretFeeEstimationInstructions(const UniValue& conf_target, cons
    57     }
    58 }
    59 
    60+std::set<int> InterpretSubtractFeeFromOutputInstructions(const UniValue& sffo_instructions, const std::vector<std::string>& destinations)
    


    S3RK commented at 8:26 am on January 10, 2024:

    in 0b0276bb00d3511172d662a94ffd7b55adbbf2d1

    I don’t see a reason to extract this function at this point (could be done later in the PR). There are two disparate cases covered (bool and vector<string>). And each case is only used in one and only one place, further commits also don’t introduce further usage of this code path as they pass vector<int>.

    Wouldn’t it be better if we keep this logic at the call site? Benefits of my proposal:

    • we don’t need second parameter for this function
    • we don’t need to pass dummy value in fundrawtransaction RPC code
    • we can combine NormalizeOutputs and ParseOutputs. they are used always together and the only reason for NormalizeOutputs to exist now is too pass second param to InterpretSubtractFeeFromOutputInstructions in send and walletfundpsbt RPC. But there second parameter is useless as subtract_fee_from_outputs option of those RPCs can’t be of the vector<string> type

    josibake commented at 11:38 am on January 10, 2024:

    I prefer having a single function (as opposed to keeping the logic at the call site) for the following reasons:

    • We avoid duplicating code at the RPC call sites:
      • The logic for validating SFFO inputs would need to be duplicated at send, fundrawtx and walletcreatefundedpsbt. This is the code that checks that the ints passed are not duplicated and in bounds and previously used to be inside rpc/FundTransaction
    • We have one place to validate all SFFO inputs:
      • Currently, sendmany SFFO inputs are not validated (documented in the added functional tests)
      • Having one function for parsing and validating makes it cleaner to add validation for all inputs in a follow-up PR. The only reason I’m not doing it in this PR is that its a behavior change (it could make calls to sendmany invalid that were previously accepted) and I’d like to keep this strictly a refactor

    we don’t need second parameter for this function

    In the case of sendmany, we do need the array of destinations to lookup their position. In the case of send, walletcreatefundedpsbt, and fundrawtransaction, we need the array of destinations to make sure our ints are in bounds. There are definitely other ways to do it, but it seems fine to use the same vector for lookups and size. More just making the point that the argument is always used.

    we can combine NormalizeOutputs and ParseOutputs. they are used always together

    Actually, the main goal here was to separate the logic in AddOutputs to get rid of duplicate validation logic in AddOutputs and also the original ParseRecipients function. sendtoaddress and sendtoaddress need the validation logic, but don’t need NormalizeOutputs since those RPCs already only allow the user to pass a key-value pair for address and amounts.

    The other RPCs (send, fundrawtransaction, walletcreatefundedpsbt) need the logic for parsing how the univalue is passed (NormalizeOutputs) and validating the addresses (ParseOutputs).

  41. S3RK commented at 9:13 am on January 15, 2024: contributor

    Code review ACK c856a1093009c6098c7c9fed5cc3f03fd0c0cf5b

    Nice improvements removing duplication between CreateRecipient and AddOutputs, also embedding SFFO values in CRecipient instead of passing as a separate parameter to FundTransaction.

    I’d still prefer InterpretSubtractFeeFromOutputInstructions to work with one type of SFFO option - vector<int> and process others at the call site. SFFO option can be in three different forms and vector<int> is the only form that used multiple times, the other forms are only used once. But that’s not blocking as InterpretSubtractFeeFromOutputInstructions and the RPCs are in the same file.

  42. DrahtBot added the label CI failed on Jan 17, 2024
  43. test: add tests for fundrawtx and sendmany rpcs
    If the serialized transaction passed to `fundrawtransaction` contains
    duplicates, they will be deserialized and added to the transaction. Add
    a test to ensure this behavior is not changed during the refactor.
    
    A user can pass any number of duplicated and unrelated addresses as an
    SFFO argument to `sendmany` and the RPC will not throw an error (note,
    all the rest of the RPCs which take SFFO as an argument will error if
    the user passes duplicates or specifies outputs not present in the
    transaction). Add a test to ensure this behavior is not changed during
    the refactor.
    435fe5cd96
  44. refactor: move normalization to new function
    Move the univalue formatting logic out of AddOutputs and into its own function,
    `NormalizeOutputs`. This allows us to re-use this logic in later commits.
    6f569ac903
  45. refactor: move parsing to new function
    Move the parsing and validation out of `AddOutputs` into its own function,
    `ParseOutputs`. This allows us to re-use this logic in `ParseRecipients` in a
    later commit, where the code is currently duplicated.
    
    The new `ParseOutputs` function returns a CTxDestination,CAmount tuples.
    This allows the caller to then translate the validated outputs into
    either CRecipients or CTxOuts.
    f7384b921c
  46. refactor: remove out param from `ParseRecipients`
    Have `ParseRecipients` return a vector of `CRecipients` and rename to `CreateRecipients`.
    47353a608d
  47. refactor: simplify `CreateRecipients`
    Move validation logic out of `CreateRecipients` and instead take the
    already validated outputs from `ParseOutputs` as an input.
    
    Move SFFO parsing out of `CreateRecipients` into a new function,
    `InterpretSubtractFeeFromOutputsInstructions`. This takes the SFFO instructions
    from `sendmany` and `sendtoaddress` and turns them into a set of integers.
    In a later commit, we will also move the SFFO parsing logic from
    `FundTransaction` into this function.
    
    Worth noting: a user can pass duplicate addresses and addresses that dont exist
    in the transaction outputs as SFFO args to `sendmany` and `sendtoaddress`
    without triggering a warning. This behavior is preserved in to keep this commit
    strictly a refactor.
    5ad19668db
  48. refactor: pass CRecipient to FundTransaction
    Instead turning tx.vout into a vector of `CRecipient`, make `FundTransaction`
    take a `CRecipient` vector directly. This allows us to remove SFFO logic from
    the wrapper RPC `FundTransaction` since the `CRecipient` objects have already
    been created with the correct SFFO values. This also allows us to remove
    SFFO from both `FundTransaction` function signatures.
    
    This sets us up in a future PR to be able to use these RPCs with BIP352
    static payment codes.
    18ad1b9142
  49. josibake force-pushed on Jan 19, 2024
  50. DrahtBot removed the label CI failed on Jan 19, 2024
  51. S3RK commented at 8:45 am on January 22, 2024: contributor
    According to my range-diff nothing changed. reACK 18ad1b9142e91cef2f5c6a693eeb2d0fbb8c517d
  52. theStack commented at 6:52 pm on January 22, 2024: contributor
    Concept ACK
  53. josibake commented at 3:36 pm on January 23, 2024: member

    According to my range-diff nothing changed. reACK 18ad1b9

    for context, this was the reason for the rebase: #29218 (comment)

  54. in src/rpc/rawtransaction_util.cpp:115 in f7384b921c outdated
    118-
    119-            CTxOut out(0, CScript() << OP_RETURN << data);
    120-            rawTx.vout.push_back(out);
    121+            CTxDestination destination{CNoDestination{CScript() << OP_RETURN << data}};
    122+            CAmount amount{0};
    123+            parsed_outputs.emplace_back(destination, amount);
    


    achow101 commented at 9:29 pm on January 23, 2024:

    In f7384b921c3460c7a3cc7827a68b2c613bd98f8e “refactor: move parsing to new function”

    nit: Making temp variables is not necessary.

  55. achow101 commented at 9:31 pm on January 23, 2024: member
    ACK 18ad1b9142e91cef2f5c6a693eeb2d0fbb8c517d
  56. DrahtBot requested review from theStack on Jan 23, 2024
  57. achow101 merged this on Jan 23, 2024
  58. achow101 closed this on Jan 23, 2024

  59. josibake deleted the branch on Jan 26, 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-21 15:12 UTC

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