detach wallet from miner #5994

pull jonasschnelli wants to merge 4 commits into bitcoin:master from jonasschnelli:2015/04/miner_wallet_separation changing 10 files +80 −62
  1. jonasschnelli commented at 11:02 am on April 10, 2015: contributor

    This will interfere with #5993. if this makes sense (conceptual), i’ll rebase this after merging of @sipa’s #5993 or @jtimon’s #4793.

    Next steps would be to add a argument pubkeyhash for generate to completely decouple. generate could check over CMainSignals if a registered “device” can provide a pubkey in case of no given key over the rpc argument.

  2. jgarzik commented at 3:31 pm on April 12, 2015: contributor

    lightly tested ACK

    Thanks for doing this. I coded this a while ago, then set it aside. Your version, with registration, is cleaner.

  3. in src/wallet/wallet.cpp: in b0c18bf9b0 outdated
    1207@@ -1208,7 +1208,7 @@ CAmount CWalletTx::GetCredit(const isminefilter& filter) const
    1208 }
    1209 
    1210 CAmount CWalletTx::GetImmatureCredit(bool fUseCache) const
    1211-{
    1212+{    
    


    Diapolo commented at 8:36 pm on April 12, 2015:
    Nit: This seems to have sneaked in?
  4. jonasschnelli commented at 1:06 pm on April 13, 2015: contributor
    fixed @Diapolo’s nits.
  5. jtimon commented at 11:25 am on April 14, 2015: contributor
    Concept ack. Given that you’re touching their interfaces already, could CreateNewBlockWithScript() ProcessBlockFound(), BitcoinMiner() and GenerateBitcoins() take const CChainParams& chainparams as parameter?
  6. jonasschnelli force-pushed on Apr 14, 2015
  7. jonasschnelli commented at 12:16 pm on April 14, 2015: contributor
    Added a commit on top to help improve global state independence by passing CChainParams for the touched functions.
  8. jonasschnelli force-pushed on Apr 14, 2015
  9. jonasschnelli force-pushed on Apr 14, 2015
  10. jtimon commented at 1:19 pm on April 14, 2015: contributor
    ut ACK, it is very nice to decouple miner.o from the wallet.
  11. jtimon commented at 1:09 am on April 16, 2015: contributor
    Needs rebase
  12. jonasschnelli force-pushed on Apr 16, 2015
  13. jonasschnelli force-pushed on Apr 16, 2015
  14. jonasschnelli commented at 7:36 am on April 16, 2015: contributor
    Rebased and squashed [squashme] commit.
  15. jonasschnelli force-pushed on Apr 16, 2015
  16. jtimon commented at 8:50 am on April 16, 2015: contributor
    I would also squash the second commit, leaving the note in the commit descrption. Specially, you never want to create CreateNewBlockWithScript. In any case, minor nit, re-utAck.
  17. jonasschnelli force-pushed on Apr 16, 2015
  18. jonasschnelli commented at 8:52 am on April 16, 2015: contributor
    Agreed. Squashed.
  19. in src/validationinterface.h: in f4dfba8952 outdated
    35@@ -35,6 +36,8 @@ class CValidationInterface {
    36     virtual void Inventory(const uint256 &hash) {};
    37     virtual void ResendWalletTransactions(int64_t nBestBlockTime) {};
    38     virtual void BlockChecked(const CBlock&, const CValidationState&) {};
    39+    virtual void GetScriptForMining(CScript &script) {};
    


    sipa commented at 9:46 am on April 24, 2015:
    I would say this needs to be able to fail (if no wallet is connected), and have the miner code deal with that case. Otherwise you can’t really make it independent of the wallet (if it needs to assume a wallet that can provide a script output for mining is present).

    jonasschnelli commented at 9:57 am on April 24, 2015:
    I think it can fail. If no listening module provides a CScript through signal ScriptForMining the miner will continue with a empty script which should result in a unspendable coinbase? But i didn’t tested that. Will do.

    sipa commented at 10:01 am on April 24, 2015:

    I would not want to create a block with an unspendable coinbase.

    I mean the call should return a boolean, or the resulting script should at least be tested for non-emptiness, and the miner should exit if that is the case.


    jonasschnelli commented at 11:33 am on April 24, 2015:

    Wouldn’t it be nice if you could use generate() in a non-wallet environment? Regarding RPC test in a post-wallet-split-off world: this could make sense.

    If somone uses the internal miner (which has no productive usage IMO) in a wallet-disable mode he probably uses it for testing only.

    I agree with adding a warning to the log. But throwing an error would block testing.


    sipa commented at 12:35 pm on April 24, 2015:
    I think the right solution for that is having a trivial not-really-a-wallet provider that explicitly allows you to set the script output to mine to. It just looks incorrect to have a provider interface for something, and a default implementation that results in the least expected result.

    jonasschnelli commented at 12:44 pm on April 24, 2015:

    Agreed. Maybe it would be most efficient by adding a argument to generate() and setgenerate() RPC calls where one could provide a output script to mine to?

    Adding a validation interface implementation for non-wallet-mining could be over the top.


    sipa commented at 12:54 pm on April 24, 2015:
    Yes, generate can just be changed to not need the GetScriptForMining interface. And setgenerate could just be deleted IMHO and replaced with a simple proof of concept Python or C miner in the tree.

    sipa commented at 12:54 pm on April 24, 2015:
    I just don’t like an interface that can clearly fail, but doesn’t have a clear way to indicate failure.
  20. in src/miner.cpp: in f4dfba8952 outdated
    540@@ -558,7 +541,7 @@ void GenerateBitcoins(bool fGenerate, CWallet* pwallet, int nThreads)
    541 
    542     minerThreads = new boost::thread_group();
    543     for (int i = 0; i < nThreads; i++)
    544-        minerThreads->create_thread(boost::bind(&BitcoinMiner, pwallet));
    545+        minerThreads->create_thread(boost::bind(&BitcoinMiner, boost::cref(chainparams)));
    


    sipa commented at 9:53 am on April 24, 2015:
    I’d rather pass a pointer to chainParams in, and pass that by value to BitcoinMiners. That makes it more obvious to the caller that the argument may be used for a longer time.
  21. in src/wallet/wallet.cpp: in f4dfba8952 outdated
    2479@@ -2480,6 +2480,17 @@ void CWallet::UpdatedTransaction(const uint256 &hashTx)
    2480     }
    2481 }
    2482 
    2483+void CWallet::GetScriptForMining(CScript &script)
    2484+{
    2485+    CReserveKey reservekey(this);
    2486+    reservekey.KeepKey();
    


    sipa commented at 9:56 am on April 24, 2015:
    Ugh, this means you’ll never get a key back, even if no block is found. Would it be possible for the wallet to keep track of which keys are currently being used in a map, and when a block is found, look up the key in the map and mark it as kept?

    jonasschnelli commented at 12:55 pm on April 24, 2015:

    IIRC there was a discussion about this on IRC. But right, i also have a bad feeling with this. Every mining thread reserves and keeps a key straight off.

    I think i’m going invest there some time and build a proper way of only keeping keys which where used when a block was created.


    jonasschnelli commented at 10:05 am on May 8, 2015:
    I think we can leave this as it is now. The only two points where this directly reserves and keeps a key is when starting the internal miner (over -gen or setgenerate, same script/key for all threads) or when calling generate.

    jonasschnelli commented at 11:47 am on June 15, 2015:
    Directly keeping the reserve key for a generate command is okay. It can result in unused but kept reserve keys if no block has been found during GenerateBitcoins() (generate, setgenerate). But this should be okay, at least after we merge HD features (https://github.com/bitcoin/bitcoin/pull/6265). And this would fix #6268

    laanwj commented at 6:16 am on June 17, 2015:
    As the internal miner is meant for testing only, this simple solution is elegant. I’d prefer not to introduce complex micro-management of keys, unless it’s necessary for anything besides the miner. Responsibility for not requesting absurd numbers of scripts can be put with the callers (e.g. the mining threads).

    gavinandresen commented at 7:46 pm on June 24, 2015:

    NACK– reservekey.KeepKey must happen AFTER .GetReservedKey or it is a no-op.

    This works:

     0diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
     1index 440459f..933b0ec 100644
     2--- a/src/wallet/wallet.cpp
     3+++ b/src/wallet/wallet.cpp
     4@@ -2586,12 +2586,12 @@ void CWallet::UpdatedTransaction(const uint256 &hashTx)
     5 void CWallet::GetScriptForMining(CScript &script)
     6 {
     7     CReserveKey reservekey(this);
     8-    reservekey.KeepKey();
     9
    10     CPubKey pubkey;
    11     if (!reservekey.GetReservedKey(pubkey))
    12         return;
    13     script = CScript() << ToByteVector(pubkey) << OP_CHECKSIG;
    14+    reservekey.KeepKey();
    15 }
    16
    17 void CWallet::LockCoin(COutPoint& output)
    

    To test: generate a block in -regtest mode, get the address of the coinbase transaction (getblock and then gettransaction). Then call getnewaddress; with the code as-is, you’ll get the same pubkey as used by the coinbase transaction.


    jonasschnelli commented at 7:57 pm on June 24, 2015:
    Hmmm… Indeed. This was a serious mistake. Thanks for tracking this down. Fixed.
  22. jonasschnelli commented at 12:24 pm on April 29, 2015: contributor

    Updated. Decoupled wallet (over signaling) from miner and rpcmining commands. It still uses signaling to allow different wallet/pubscript-generation implementations, though, mining without pubScript providing listener will generate a error and stops mining.

    Now it reserves and keeps a key when calling setgenerate or generating over cmd arg gen (same key for all generated blocks).

  23. jonasschnelli force-pushed on Apr 29, 2015
  24. jtimon commented at 11:17 am on April 30, 2015: contributor
    Travis is failing for the build without wallet: https://travis-ci.org/bitcoin/bitcoin/jobs/60528797#L1557
  25. jonasschnelli force-pushed on Apr 30, 2015
  26. jonasschnelli commented at 11:52 am on April 30, 2015: contributor
    @jtimon: Thanks for the report. Fixed.
  27. jonasschnelli commented at 12:23 pm on April 30, 2015: contributor

    Now the miner is completely decoupled from the wallet. There are no more #ifdef ENABLE_WALLET checks (for mining). The only check is during runtime if there was a listening device who could provide a CScript over the signal ScriptForMining().

    If one tries to use -gen without a wallet compiled in or enabled, it then will throw an exception during init phase. Current master would just ignore the cmd arg -gen if no wallet is present.

    The RPC commands setgenerate, generate, getgenerate works in wallet-disabled mode but also throw a exception error: {"code":-32601,"message":"Use the generate method instead of setgenerate on this network"}

  28. jonasschnelli force-pushed on May 19, 2015
  29. jonasschnelli commented at 11:30 am on May 19, 2015: contributor
    Rebased. Seeks testers to escape the rebase hamster wheel.
  30. jonasschnelli force-pushed on May 19, 2015
  31. in src/miner.cpp: in 6a30073b4c outdated
    548@@ -571,9 +549,14 @@ void GenerateBitcoins(bool fGenerate, CWallet* pwallet, int nThreads)
    549     if (nThreads == 0 || !fGenerate)
    550         return;
    551 
    552+    CScript coinbaseScript;
    553+    GetMainSignals().ScriptForMining(coinbaseScript);
    554+
    555+    //throw an error if no script was provided
    


    Diapolo commented at 12:26 pm on May 19, 2015:
    Nit: Missing a space after // Edit: Same nit in rpcmining.cpp.
  32. Diapolo commented at 12:30 pm on May 19, 2015: none
    Can you extend or explain in some more details, how this pull achieves, what it sais in the commit-msg title? Perhaps in the commit-msg itself.
  33. jonasschnelli commented at 1:16 pm on May 19, 2015: contributor
    @Diapolo: this PR is a step forward in separating the wallet from the core which can lead to multiple benefits. This would be required to allow a flexible interface (basic modularity) for adding/switching bitcoin-core wallets.
  34. jonasschnelli force-pushed on May 28, 2015
  35. jonasschnelli force-pushed on Jun 1, 2015
  36. in src/miner.cpp: in 41f62d1b23 outdated
    550@@ -573,9 +551,14 @@ void GenerateBitcoins(bool fGenerate, CWallet* pwallet, int nThreads)
    551     if (nThreads == 0 || !fGenerate)
    552         return;
    553 
    554+    CScript coinbaseScript;
    555+    GetMainSignals().ScriptForMining(coinbaseScript);
    


    luke-jr commented at 2:33 am on June 2, 2015:
    Are signals guaranteed to be synchronous?

    jonasschnelli commented at 4:09 am on June 2, 2015:
    Yes. Boost signals2 are always synchronous and in strict order (first register first call).

    luke-jr commented at 5:19 am on June 2, 2015:
    Is this guaranteed/by design, or merely an implementation coincidence?

    jonasschnelli commented at 6:41 pm on June 2, 2015:
    IIRC boost signals2 are by design synchronous. Check http://www.boost.org/doc/libs/1_58_0/doc/html/signals2/thread-safety.html

    luke-jr commented at 8:33 pm on June 2, 2015:
    Nothing on that page suggests they are synchronous…?

    jonasschnelli commented at 9:37 am on June 3, 2015:

    Maybe my backtrace prove (see below) can convince you. I can also post a linux gdb backtrace or you could add a sleep(10) invoid CWallet::GetScriptForMining(CScript &script)`.

    bildschirmfoto 2015-06-03 um 11 34 37


    luke-jr commented at 6:28 pm on June 3, 2015:
    That’s with a current version of Boost. Nothing I can see prevents Boost 1.90 from making it all asynchronous.

    sipa commented at 8:15 pm on June 3, 2015:
    What would asynchronous even mean in this context? You can specify how signal’s return values are combined. There is no way to make the execution asynchronous without breaking the API (which returns the combined return value directly).

    luke-jr commented at 8:38 pm on June 3, 2015:
    @sipa That’s n/a for void return values. Maybe the solution here is to return the CScript then?

    sipa commented at 8:50 pm on June 3, 2015:
    I think we can very reasonably assume that boost::signals2 will not fundamentally change their design and API in a way that will only remain compatible for void outputs.

    jonasschnelli commented at 10:20 pm on June 3, 2015:
    @luke-jr: if the signal listener don’t provide a script, a exception get thrown at L559. If there are multiple listeners, one listener can detect this by reading out the passed-by-reference coinbaseScript (check if it’s empty, etc.) and skip the creation/generation of another CScript or do some other clever things. IMO this design is acceptable for the internal miner/wallet decoupling.
  37. sipa commented at 2:07 pm on June 14, 2015: member
    Needs rebase.
  38. jonasschnelli force-pushed on Jun 15, 2015
  39. jonasschnelli commented at 11:44 am on June 15, 2015: contributor
    Rebased.
  40. in src/miner.cpp: in 7fb5dcb76b outdated
    559+        throw std::runtime_error("No coinbase script available (mining requires a wallet)");
    560+
    561     minerThreads = new boost::thread_group();
    562     for (int i = 0; i < nThreads; i++)
    563-        minerThreads->create_thread(boost::bind(&BitcoinMiner, pwallet));
    564+        minerThreads->create_thread(boost::bind(&BitcoinMiner, boost::cref(chainparams), coinbaseScript));
    


    sipa commented at 11:52 am on June 15, 2015:
    Maybe I’m alone with this, but I dislike boost::cref… it seems like a hack to make pass-by-reference work when passing a pointer would work just as well.

    Diapolo commented at 12:57 pm on June 15, 2015:
    So a hack for something that’s causes by a limitation in Boost itself? Weird…

    laanwj commented at 6:20 am on June 17, 2015:
    Passing a pointer would be fine with me, I don’t feel strongly about it. I suppose this will be replaced with C++11 constructs (lambda’s?) when the time is there.

    jonasschnelli commented at 6:24 am on June 17, 2015:
    I saw that the new scheduler also introduced boost::cref and i don’t have a strong feeling about it (in both directions). Maybe it’s worth fixing both as soon as we switch to c++11?

    jtimon commented at 6:40 am on June 17, 2015:
    I don’t have a strong opinion either, but I plan to copy whatever is done here for other functions in main.
  41. sipa commented at 11:55 am on June 15, 2015: member
    Untested ACK.
  42. jtimon commented at 12:35 pm on June 15, 2015: contributor
    re-untestedACK
  43. in src/validationinterface.h: in 7fb5dcb76b outdated
    34@@ -34,6 +35,8 @@ class CValidationInterface {
    35     virtual void Inventory(const uint256 &hash) {}
    36     virtual void ResendWalletTransactions(int64_t nBestBlockTime) {}
    37     virtual void BlockChecked(const CBlock&, const CValidationState&) {}
    38+    virtual void GetScriptForMining(CScript &script) {};
    


    laanwj commented at 6:11 am on June 17, 2015:

    Why not make the return argument explicit:

    0CScript GetScriptForMining();
    

    Yes - you’ll need to specify a combiner for the return argument, but right now this issue (“what if there are multiple subscribers that return a value”) is hidden.


    jonasschnelli commented at 6:22 am on June 17, 2015:

    I think the current solution is elegant. If multiple subscribers are present, listeners can check the current script value and overwrite or change it. Combining return values (adding a combiner template) would mean to add a logic into the miner to decide, which scripts should be taken if multiple where supplied.

    But sure, this have a downside of the fact, that all non-last signal listeners can’t make sure that its script get taken.


    laanwj commented at 6:47 am on June 17, 2015:

    I tend to disagree that it’s the responsibility of the listeners to handle what to do when there are multiple listeners - you may not always have control over in which order they are invoked, and that can result in hard to debug problems.

    I’m not sure we even want to have multiple subscribers here, ever? If not, a combiner could simply throw an error if multiple return values are returned.


    jonasschnelli commented at 6:54 am on June 17, 2015:

    Lets say if we support two wallets, we could define which wallet takes the signaling-lead by setting the signal registration order (last signal connect = last signal call).

    Throwing an error because both wallets could provide a script for mining seems to be relatively strict. The current solution would allow the 2nd (or upcoming) wallet(s) to decide if he likes to take the lead and overwrite or allow the first wallets script.

    Adding a combiner would mean that we somehow had to check, which wallet provided which script. Also in that case the miner is in charge to decide which script should be taken.

  44. jonasschnelli force-pushed on Jun 24, 2015
  45. jonasschnelli force-pushed on Jun 24, 2015
  46. in src/wallet/wallet.cpp: in d65e9a84e7 outdated
    2582@@ -2583,6 +2583,16 @@ void CWallet::UpdatedTransaction(const uint256 &hashTx)
    2583     }
    2584 }
    2585 
    2586+void CWallet::GetScriptForMining(CScript &script)
    2587+{
    2588+    CReserveKey reservekey(this);
    


    laanwj commented at 12:04 pm on June 30, 2015:

    Why not make CReserveKey (or e.g. CReserveScript) an abstract base class defined outside the wallet, and return that from CWallet::GetScriptForMining, so that like now the caller can decide to keep or return it

    This also interacts better with the case where multiple wallets return a script for mining and some have to be discarded. At least with a CReserveKey-like class, these keys will be recycled.


    jonasschnelli commented at 7:02 pm on June 30, 2015:
    Yes. This current design does not respect the keypool/CReserveKey. Will try a better approach.
  47. laanwj added the label Refactoring on Jun 30, 2015
  48. detach wallet from miner d0fc10a844
  49. fix GetScriptForMining() CReserveKey::keepKey() issue 087e65def9
  50. jonasschnelli force-pushed on Jul 1, 2015
  51. jonasschnelli force-pushed on Jul 1, 2015
  52. jonasschnelli force-pushed on Jul 1, 2015
  53. jonasschnelli force-pushed on Jul 1, 2015
  54. jonasschnelli force-pushed on Jul 1, 2015
  55. jonasschnelli force-pushed on Jul 1, 2015
  56. add CReserveScript to allow modular script keeping/returning
    - use one CReserveScript per mining thread
    5496253966
  57. in src/validationinterface.h: in cc304ec8fe outdated
    35@@ -34,6 +36,8 @@ class CValidationInterface {
    36     virtual void Inventory(const uint256 &hash) {}
    37     virtual void ResendWalletTransactions(int64_t nBestBlockTime) {}
    38     virtual void BlockChecked(const CBlock&, const CValidationState&) {}
    39+    virtual void GetScriptForMining(boost::shared_ptr<CReserveScript>&) {};
    40+    virtual void UpdateRequestCount(const CBlock&) {};
    


    laanwj commented at 12:32 pm on July 1, 2015:
    Note that Inventory(const uint256 &hash), connected to g_signals.Inventory does exactly the same thing. As far as I see we can get rid of UpdateRequestCount and replace it with that.

    jonasschnelli commented at 2:11 pm on July 1, 2015:
    Renamed UpdateRequestCount (name does not reflect the purpose of the method) to ResetRequestCount.
  58. in src/script/script.h: in cc304ec8fe outdated
    608@@ -609,4 +609,13 @@ class CScript : public std::vector<unsigned char>
    609     }
    610 };
    611 
    612+class CReserveScript
    613+{
    614+public:
    615+    CScript reserveScript;
    616+    virtual void KeepScript() {};
    


    laanwj commented at 12:33 pm on July 1, 2015:
    Redundant ;

    jonasschnelli commented at 2:07 pm on July 1, 2015:
    Fixed.
  59. jonasschnelli force-pushed on Jul 1, 2015
  60. jonasschnelli force-pushed on Jul 1, 2015
  61. jonasschnelli force-pushed on Jul 1, 2015
  62. miner: rename UpdateRequestCount signal to ResetRequestCount a7b9623d18
  63. jonasschnelli force-pushed on Jul 1, 2015
  64. laanwj merged this on Jul 1, 2015
  65. laanwj closed this on Jul 1, 2015

  66. laanwj referenced this in commit 786ed114c2 on Jul 1, 2015
  67. laanwj commented at 4:39 pm on July 1, 2015: member
    ACK
  68. jtoomim referenced this in commit 27de90454f on Feb 9, 2016
  69. zkbot referenced this in commit 6dcc7dd6ce on Mar 9, 2018
  70. str4d referenced this in commit d67af6dfec on Mar 10, 2018
  71. zkbot referenced this in commit b094b02c0b on Mar 10, 2018
  72. str4d referenced this in commit 5d7dbf134a on Mar 12, 2018
  73. zkbot referenced this in commit 59ef0e4c9c on Mar 12, 2018
  74. str4d referenced this in commit 6c29dddd08 on Mar 12, 2018
  75. zkbot referenced this in commit 6dfd4baa1a on Mar 12, 2018
  76. str4d referenced this in commit c9caf081f2 on Mar 12, 2018
  77. str4d referenced this in commit 6866ac151c on Mar 15, 2018
  78. str4d referenced this in commit abea079a3b on Mar 30, 2018
  79. str4d referenced this in commit 7700fd4a43 on Apr 4, 2018
  80. zkbot referenced this in commit 05132fdca8 on Apr 6, 2018
  81. str4d referenced this in commit 36e5497a00 on Apr 13, 2018
  82. zkbot referenced this in commit 5fefc451a7 on Apr 13, 2018
  83. str4d referenced this in commit 4e89d158d5 on May 31, 2018
  84. zkbot referenced this in commit 4c91fbf1c7 on May 31, 2018
  85. str4d referenced this in commit af73434f2f on Oct 24, 2018
  86. zkbot referenced this in commit 82d64f57c9 on Oct 24, 2018
  87. str4d referenced this in commit c4d442a57b on Oct 25, 2018
  88. zkbot referenced this in commit 971f37e510 on Oct 25, 2018
  89. str4d referenced this in commit 884a21b1bc on Jan 4, 2019
  90. str4d referenced this in commit f1fa2a4573 on Jan 4, 2019
  91. zkbot referenced this in commit e8d8897c62 on Jan 30, 2019
  92. str4d referenced this in commit b0f1d643aa on Mar 5, 2019
  93. zkbot referenced this in commit 224635bba7 on Mar 12, 2019
  94. zkbot referenced this in commit 1cbe5075d6 on Mar 12, 2019
  95. Eirik0 referenced this in commit 168896f32a on Mar 14, 2019
  96. renuzit referenced this in commit d96755adbb on May 16, 2019
  97. milesmanley referenced this in commit 7eccff99ef on May 19, 2019
  98. milesmanley referenced this in commit 720ae8a9db on May 19, 2019
  99. garethtdavies referenced this in commit 6e0853a19a on May 30, 2019
  100. garethtdavies referenced this in commit b3c3208606 on May 30, 2019
  101. garethtdavies referenced this in commit 5a4eb49e91 on May 30, 2019
  102. renuzit referenced this in commit e864cba9be on May 31, 2019
  103. DrahtBot locked this on Sep 8, 2021

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-12-19 00:12 UTC

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