rest: allow reading partial block data from storage #33657

pull romanz wants to merge 1 commits into bitcoin:master from romanz:romanz/rest-blockdata changing 7 files +174 −7
  1. romanz commented at 11:08 am on October 19, 2025: contributor

    It allows fetching specific transactions using an external index, following #32541 (comment).

    Currently, electrs and other indexers map between an address/scripthash to the list of the relevant transactions.

    However, in order to fetch those transactions from bitcoind, electrs relies on reading the whole block and post-filtering for a specific transaction1. Other indexers use a txindex to fetch a transaction using its txid 234.

    The above approach has significant storage and CPU overhead, since the txid is a pseudo-random 32-byte value. Also, mainnet txindex takes ~60GB today.

    This PR is adding support for using the transaction’s position within its block to be able to fetch it directly using REST API, using the following HTTP request:

    0GET /rest/blockpart/BLOCKHASH.bin?offset=OFFSET&size=SIZE
    

    The offsets’ index can be encoded much more efficiently (<3GB today).

    Only binary and hex response formats are supported.

  2. DrahtBot added the label RPC/REST/ZMQ on Oct 19, 2025
  3. DrahtBot commented at 11:08 am on October 19, 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/33657.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK optout21
    Stale ACK ubbabeck, TheCharlatan

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

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • GET /rest/blockpart/<BLOCK-HASH>.<bin|hex>?start=X&offset=Y -> GET /rest/blockpart/<BLOCK-HASH>.<bin|hex>?offset=X&size=Y [The added test and implementation use query parameters “offset” and “size”; “start” and “offset” is inconsistent and would confuse readers about the actual parameters.]

    • Given a block hash: returns a block part, in binary or hex-encoded binary formats. -> Given a block hash, returns a block part in binary or hex-encoded binary formats. [Replace the colon with a comma and remove the extra comma before “in” for correct, comprehensible sentence structure.]

    drahtbot_id_5_m

  4. romanz force-pushed on Oct 19, 2025
  5. andrewtoth commented at 9:30 pm on October 19, 2025: contributor

    Some of the PR description is reused from the previous PR, but is now stale.

    This PR is adding support for using the transaction’s position within its block to be able to fetch it directly using REST API, using the following HTTP request (to fetch the N-th transaction from BLOCKHASH):

    This does not support fetching an N-th transaction, only a slice of a block.

    If binary response format is used, the transaction data will be read directly from the storage and sent back to the client, without any deserialization overhead.

    There is no way to use this in non-binary form (except hex I suppose).

  6. in src/node/blockstorage.cpp:1096 in 787d6df48a
    1093+            offset = *part_offset;
    1094+        }
    1095+
    1096+        size_t size = blk_size - offset;
    1097+        if (part_size.has_value()) {
    1098+            if (*part_size > size) {
    


    andrewtoth commented at 11:04 pm on October 19, 2025:
    Do we want to support zero *part_size? Should probably return false too?

    romanz commented at 8:21 pm on October 20, 2025:
    Sounds good - fixed in 787d6df48a..c4bf7a77c4.

    andrewtoth commented at 10:22 pm on October 20, 2025:
    We should probably cover this as well as other cases of ReadRawBlockPart in blockmanager_tests.cpp.

    romanz commented at 8:26 pm on October 21, 2025:
  7. in src/node/blockstorage.h:417 in 787d6df48a
    413@@ -414,6 +414,7 @@ class BlockManager
    414     bool ReadBlock(CBlock& block, const FlatFilePos& pos, const std::optional<uint256>& expected_hash) const;
    415     bool ReadBlock(CBlock& block, const CBlockIndex& index) const;
    416     bool ReadRawBlock(std::vector<std::byte>& block, const FlatFilePos& pos) const;
    417+    bool ReadRawBlockPart(std::vector<std::byte>& block, const FlatFilePos& pos, std::optional<size_t> part_offset, std::optional<size_t> part_size) const;
    


    optout21 commented at 11:26 am on October 20, 2025:
    nit: Declaration differs from definition, block vs data parameter name

    romanz commented at 8:21 pm on October 20, 2025:
    Thanks - fixed in 787d6df48a..c4bf7a77c4.
  8. in src/rest.cpp:486 in 787d6df48a outdated
    495@@ -472,6 +496,11 @@ static bool rest_block_notxdetails(const std::any& context, HTTPRequest* req, co
    496     return rest_block(context, req, uri_part, TxVerbosity::SHOW_TXID);
    497 }
    498 
    499+static bool rest_block_part(const std::any& context, HTTPRequest* req, const std::string& uri_part)
    


    optout21 commented at 11:31 am on October 20, 2025:
    This way both block and blockpart api’s take the same paramaters, including "offset" and "size", right? Is that intended?

    romanz commented at 8:14 pm on October 20, 2025:
    Right - I have used the same implementation for both endpoints for simplicity and to avoid code duplication. I can add an additional boolean argument to enable offset and size parameter handling for the /rest/blockpart endpoint (and disable it for /rest/block endpoint). WDYT?

    andrewtoth commented at 9:28 pm on October 20, 2025:

    If I understand correctly, this would allow passing offset or size to /rest/block along with .json format specification. Normally this would fail to deserialize, but if very unlucky it could deserialize to what looks like a block and return? That would be very bad. Similarly if not using .json, this would return incorrect data that a user might not expect (for instance if they switched rest endpoints but forgot to remove parameters).

    I would recommend disabling these parameters if not calling /rest/blockpart.


    romanz commented at 6:01 am on October 21, 2025:
    Agree - fixed in c4bf7a77c4..c9bfd298be (offset and size are parsed only for /rest/blockpart endpoint).
  9. romanz force-pushed on Oct 20, 2025
  10. romanz commented at 8:23 pm on October 20, 2025: contributor

    Some of the PR description is reused from the previous PR, but is now stale.

    Good catch, thanks! Updated PR description: #33657#issue-3529967048

  11. romanz force-pushed on Oct 21, 2025
  12. optout21 commented at 9:48 am on October 21, 2025: none
    Concept ACK c9bfd298be422de7e989fe244fb4281c507068a3
  13. romanz force-pushed on Oct 21, 2025
  14. romanz requested review from andrewtoth on Oct 22, 2025
  15. ubbabeck commented at 7:33 pm on October 22, 2025: none

    tACK 301116e855

    What was done:

    • functional test.
    • unit test
    • tested the new rest endpoint rest/blockpart/<blockhash>.bin manually with curl with different offsets and sizes.

    If there are any additional test you’d like me to help with let me know. I also did a perf dump, but I need some guidelines on what to measure/ grep for.

  16. DrahtBot requested review from optout21 on Oct 22, 2025
  17. romanz commented at 9:44 pm on October 22, 2025: contributor

    Many thanks @ubbabeck!

    If there are any additional test you’d like me to help with let me know.

    It would be interesting to compare the performance for single transaction fetching, similar to how it was done in #32541 (comment), comparing txindex-based and locationindex-based queries.

  18. ubbabeck commented at 5:12 pm on October 23, 2025: none

    It would be interesting to compare the performance for single transaction fetching, similar to how it was done in #32541 (comment), comparing txindex-based and locationindex-based queries.

    Here are the results from testing. I used hey to benchmark and your rust getrawtransaction.

    Was this somewhat you were looking for? If not let me know and I’ll redo them correctly.

    Maybe not relevant to test, but fetching the whole block using blockpart.

    hey -c 1 -n 100000 http://localhost:8332/rest/blockpart/000000000000000000017bfd05b5fa367a424c4a565a4baf7950d9e8605df8ec.bin

     0Summary:
     1  Total:	114.4020 secs
     2  Slowest:	0.0048 secs
     3  Fastest:	0.0009 secs
     4  Average:	0.0011 secs
     5  Requests/sec:	874.1103
     6
     7  Total data:	146938500000 bytes
     8  Size/request:	1469385 bytes
     9
    10Response time histogram:
    11  0.001 [1]	|
    12  0.001 [93859]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
    13  0.002 [4835]	|■■
    14  0.002 [1052]	|
    15  0.002 [165]	|
    16  0.003 [65]	|
    17  0.003 [20]	|
    18  0.004 [2]	|
    19  0.004 [0]	|
    20  0.004 [0]	|
    21  0.005 [1]	|
    22
    23
    24Latency distribution:
    25  10% in 0.0010 secs
    26  25% in 0.0011 secs
    27  50% in 0.0011 secs
    28  75% in 0.0012 secs
    29  90% in 0.0013 secs
    30  95% in 0.0013 secs
    31  99% in 0.0018 secs
    32
    33Details (average, fastest, slowest):
    34  DNS+dialup:	0.0000 secs, 0.0009 secs, 0.0048 secs
    35  DNS-lookup:	0.0000 secs, 0.0000 secs, 0.0006 secs
    36  req write:	0.0000 secs, 0.0000 secs, 0.0003 secs
    37  resp wait:	0.0004 secs, 0.0002 secs, 0.0034 secs
    38  resp read:	0.0007 secs, 0.0002 secs, 0.0030 secs
    39
    40Status code distribution:
    41  [200]	100000 responses
    

    Using a random offset and size of 310 bytes to simulate fetching a transaction.

    hey -c 16 -n 100000 http://localhost:8332/rest/blockpart/000000000000000000017bfd05b5fa367a424c4a565a4baf7950d9e8605df8ec.bin\?offset\=4000\&size\=310

     0Summary:
     1  Total:	1.7338 secs
     2  Slowest:	0.0023 secs
     3  Fastest:	0.0001 secs
     4  Average:	0.0003 secs
     5  Requests/sec:	57675.3814
     6  
     7  Total data:	31000000 bytes
     8  Size/request:	310 bytes
     9
    10Response time histogram:
    11  0.000 [1]	|
    12  0.000 [57797]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
    13  0.000 [41217]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
    14  0.001 [824]	|■
    15  0.001 [76]	|
    16  0.001 [10]	|
    17  0.001 [15]	|
    18  0.002 [20]	|
    19  0.002 [24]	|
    20  0.002 [10]	|
    21  0.002 [6]	|
    22
    23
    24Latency distribution:
    25  10% in 0.0002 secs
    26  25% in 0.0003 secs
    27  50% in 0.0003 secs
    28  75% in 0.0003 secs
    29  90% in 0.0003 secs
    30  95% in 0.0003 secs
    31  99% in 0.0005 secs
    32
    33Details (average, fastest, slowest):
    34  DNS+dialup:	0.0000 secs, 0.0001 secs, 0.0023 secs
    35  DNS-lookup:	0.0000 secs, 0.0000 secs, 0.0015 secs
    36  req write:	0.0000 secs, 0.0000 secs, 0.0008 secs
    37  resp wait:	0.0003 secs, 0.0000 secs, 0.0018 secs
    38  resp read:	0.0000 secs, 0.0000 secs, 0.0007 secs
    39
    40Status code distribution:
    41  [200]	100000 responses
    

    One runner

    hey -c 1 -n 100000 http://localhost:8332/rest/blockpart/000000000000000000017bfd05b5fa367a424c4a565a4baf7950d9e8605df8ec.bin\?offset\=4000\&size\=310

     0Summary:
     1  Total:	1.7338 secs
     2  Slowest:	0.0023 secs
     3  Fastest:	0.0001 secs
     4  Average:	0.0003 secs
     5  Requests/sec:	57675.3814
     6  
     7  Total data:	31000000 bytes
     8  Size/request:	310 bytes
     9
    10Response time histogram:
    11  0.000 [1]	|
    12  0.000 [57797]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
    13  0.000 [41217]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
    14  0.001 [824]	|■
    15  0.001 [76]	|
    16  0.001 [10]	|
    17  0.001 [15]	|
    18  0.002 [20]	|
    19  0.002 [24]	|
    20  0.002 [10]	|
    21  0.002 [6]	|
    22
    23
    24Latency distribution:
    25  10% in 0.0002 secs
    26  25% in 0.0003 secs
    27  50% in 0.0003 secs
    28  75% in 0.0003 secs
    29  90% in 0.0003 secs
    30  95% in 0.0003 secs
    31  99% in 0.0005 secs
    32
    33Details (average, fastest, slowest):
    34  DNS+dialup:	0.0000 secs, 0.0001 secs, 0.0023 secs
    35  DNS-lookup:	0.0000 secs, 0.0000 secs, 0.0015 secs
    36  req write:	0.0000 secs, 0.0000 secs, 0.0008 secs
    37  resp wait:	0.0003 secs, 0.0000 secs, 0.0018 secs
    38  resp read:	0.0000 secs, 0.0000 secs, 0.0007 secs
    39
    40Status code distribution:
    41  [200]	100000 responses
    

    Rust getrawtransaction benchmark

    0time cargo run --release -- 4137d0dbad434d68a4f52b7bebcba91ddac3f7f5c92b84130432bd6b5e2ea57a 0000000000000000000083a0cff38278aae196d6d923a7e8ee7e5a0e371226fe
    1   Compiling bench-getrawtx v0.1.0 (review/bench-getrawtx)
    2    Finished `release` profile [optimized] target(s) in 0.29s
    3     Running `target/x86_64-unknown-linux-gnu/release/bench-getrawtx 4137d0dbad434d68a4f52b7bebcba91ddac3f7f5c92b84130432bd6b5e2ea57a 0000000000000000000083a0cff38278aae196d6d923a7e8ee7e5a0e371226fe`
    4iterations = 100000
    5average RPC duration = 130.319µs
    6cargo run --release --    1.29s user 1.09s system 17% cpu 13.342 total
    
  19. romanz commented at 9:43 pm on October 25, 2025: contributor

    Tested the performance of the new REST endpoint using ab on mainnet with various SIZE parameters:

    0$ BLOCKHASH=00000000000000000001eae3683a5350b67ddb17d9c7b6c8010ab5b36ccbaa09
    1$ ab -q -k -c 1 -n 100000 'http://localhost:8332/rest/blockpart/BLOCKHASH.bin?offset=0&size=SIZE'
    
    SIZE Time per request [ms]
    1 0.03
    10 0.03
    100 0.03
    1k 0.03
    10k 0.04
    100k 0.07
    1M 0.6

    So fetching a typical transaction is expected to take <40us on my machine. @ubbabeck could you please test it too?

  20. andrewtoth commented at 10:19 pm on October 25, 2025: contributor

    Compared your command fetching SIZE=788 and block 850000 with a 788 byte tx in that block 2d2dcc80195541dd44209dcfeb25393c8c8710f262360368a618b7ff3fa3f08c using getrawtransaction with txindex. I used hex encoding to make the comparison more fair.

    0ab -q -k -c 1 -n 1000000 'http://localhost:8332/rest/blockpart/00000000000000000002a0b5db2a7f8d9087464c2586b546be7bce8eb53b8187.hex?offset=0&size=788'
    

    I got Time per request: 0.037 [ms] (mean).

    Compared with RPC getrawtransaction:

    0ab -q -k -c 1 -n 1000000 -p data.json -A user:password 'http://localhost:8332/'
    

    where user:password are what is set in the config and data.json is

    0{"jsonrpc": "1.0", "id": "curltest", "method": "getrawtransaction", "params": ["2d2dcc80195541dd44209dcfeb25393c8c8710f262360368a618b7ff3fa3f08c"]}
    

    I got Time per request: 0.135 [ms] (mean). So quite a bit faster with the blockpart method.

    Using binary encoding Time per request: 0.032 [ms] (mean). Slightly faster.

  21. ubbabeck commented at 1:39 pm on October 26, 2025: none

    @ubbabeck could you please test it too?

    Sure here her my results for the same test using the same params and ApacheBench, Version 2.3

    SIZE Time per request [ms]
    1 0.046
    10 0.046
    100 0.050
    1k 0.050
    10k 0.054
    100k 0.113
    1m 0.803

    Fetching on average tx on my system is expected to take <0.05 ms

    For fetching 2d2dcc80195541dd44209dcfeb25393c8c8710f262360368a618b7ff3fa3f08c as Andrew did.

    Blockpart ms rpc getrawtransaction ms diff
    0.022 0.104 0.082
  22. in src/rest.cpp:506 in 301116e855 outdated
    497+            if (!part_size.has_value()) {
    498+                return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Block part size is invalid: %s", *part_size_str));
    499+            }
    500+        }
    501+    } catch (const std::runtime_error& e) {
    502+        return RESTERR(req, HTTP_BAD_REQUEST, e.what());
    


    TheCharlatan commented at 12:35 pm on October 30, 2025:
    Can you add a test case for the error conditions? Maybe I am missing it, but corecheck’s coverage report also thinks they are not covered.

    romanz commented at 8:59 pm on October 30, 2025:

    Thanks - added in 301116e855…c30647c4d3.

    Also, tested an invalid RFC 3986 query:

     0$ curl -v 'http://localhost:8332/rest/blockpart/000000000000000000016d31a675b96acb4aacca6e4653039703b9fe03997c78.hex?%XY'
     1* Host localhost:8332 was resolved.
     2* IPv6: ::1
     3* IPv4: 127.0.0.1
     4*   Trying [::1]:8332...
     5* Connected to localhost (::1) port 8332
     6> GET /rest/blockpart/000000000000000000016d31a675b96acb4aacca6e4653039703b9fe03997c78.hex?%XY HTTP/1.1
     7> Host: localhost:8332
     8> User-Agent: curl/8.5.0
     9> Accept: */*
    10> 
    11< HTTP/1.1 400 Bad Request
    12< Content-Type: text/plain
    13< Date: Thu, 30 Oct 2025 20:58:12 GMT
    14< Content-Length: 69
    15< 
    16URI parsing failed, it likely contained RFC 3986 invalid characters
    17* Connection [#0](/bitcoin-bitcoin/0/) to host localhost left intact
    

    as well as querying a pruned block:

     0$ curl -v 'http://localhost:8332/rest/blockpart/0000000000002917ed80650c6174aac8dfc46f5fe36480aaef682ff6cd83c3ca.hex?offset=3&size=5'
     1* Host localhost:8332 was resolved.
     2* IPv6: ::1
     3* IPv4: 127.0.0.1
     4*   Trying [::1]:8332...
     5* Connected to localhost (::1) port 8332
     6> GET /rest/blockpart/0000000000002917ed80650c6174aac8dfc46f5fe36480aaef682ff6cd83c3ca.hex?offset=3&size=5 HTTP/1.1
     7> Host: localhost:8332
     8> User-Agent: curl/8.5.0
     9> Accept: */*
    10> 
    11< HTTP/1.1 404 Not Found
    12< Content-Type: text/plain
    13< Date: Thu, 30 Oct 2025 20:59:08 GMT
    14< Content-Length: 94
    15< 
    160000000000002917ed80650c6174aac8dfc46f5fe36480aaef682ff6cd83c3ca not available (pruned data)
    17* Connection [#0](/bitcoin-bitcoin/0/) to host localhost left intact
    
  23. TheCharlatan commented at 12:53 pm on October 30, 2025: contributor
    Made sure this also doesn’t slow down normal block retrieval. There is a bit of extra logic in the hot path for ReadRawBlock, but I think this should be fine.
  24. romanz force-pushed on Oct 30, 2025
  25. TheCharlatan approved
  26. TheCharlatan commented at 8:17 am on October 31, 2025: contributor
    ACK c30647c4d34c2941696729704854467b30657c43
  27. in src/rest.cpp:401 in c30647c4d3 outdated
    391@@ -392,7 +392,9 @@ static bool rest_spent_txouts(const std::any& context, HTTPRequest* req, const s
    392 static bool rest_block(const std::any& context,
    393                        HTTPRequest* req,
    394                        const std::string& uri_part,
    395-                       TxVerbosity tx_verbosity)
    396+                       std::optional<TxVerbosity> tx_verbosity,
    


    oren-z0 commented at 2:02 pm on November 1, 2025:
    Why make verbosity optional, and not keep as-is? Where is it called with a null value?

    romanz commented at 2:39 pm on November 1, 2025:

    Why make verbosity optional, and not keep as-is?

    It allows reusing this function also for the new REST endpoint (/blockpart/) which doesn’t support JSON response format.

    Where is it called with a null value?

    It is called by rest_block_part() here: https://github.com/bitcoin/bitcoin/blob/c30647c4d34c2941696729704854467b30657c43/src/rest.cpp#L480-L503


    oren-z0 commented at 10:25 pm on November 1, 2025:
    Oh, now I get it. So the question is why tx_verbosity was required in the first place even when the data-format was binary or hex (where it is ignored). Consider splitting this into two commits, one to make tx_verbosity optional, and another to add the new blockpart feature.

    romanz commented at 6:02 pm on November 2, 2025:
    Not sure if splitting this PR into two commits will make it clearer… Maybe I should add a comment at rest_block() definition?

    romanz commented at 6:14 pm on November 2, 2025:

    e.g.:

     0/**
     1 * This handler is used for multiple HTTP endpoints:
     2 * - `/block/` via `rest_block_extended()`
     3 * - `/block/notxdetails/` via `rest_block_notxdetails()`
     4 * - `/blockpart/` via `rest_block_part` (doesn't support JSON response so `tx_verbosity` is unset)
     5 */
     6static bool rest_block(const std::any& context,
     7                       HTTPRequest* req,
     8                       const std::string& uri_part,
     9                       std::optional<TxVerbosity> tx_verbosity,
    10                       std::optional<size_t> part_offset,
    11                       std::optional<size_t> part_size)
    

  28. andrewtoth commented at 7:26 pm on November 2, 2025: contributor
    @romanz do you have a proof-of-concept for the tx indexer that will utilize this? It would be good to make sure this will be sufficient and performant enough for your requirements.
  29. romanz commented at 9:41 pm on November 3, 2025: contributor

    do you have a proof-of-concept for the tx indexer that will utilize this?

    Yes - I am working on a PR to adapt bindex to use the new REST API endpoint. Assuming warm OS block cache, https://github.com/romanz/bindex-rs/pull/63#issuecomment-3448841256 achieves retrieval rate of ~10k txs/second when querying a “popular” signet address.

    Will test it on mainnet as well :)

  30. romanz commented at 6:33 am on November 4, 2025: contributor
    Tested mainnet query performance using https://mempool.space/address/1BitcoinEaterAddressDontSendf59kuE. Assuming warm OS block cache, https://github.com/romanz/bindex-rs/pull/63#issuecomment-3484098889 takes <1s to fetch 5190 txs, i.e. <0.2ms per tx :+1:
  31. romanz force-pushed on Nov 8, 2025
  32. romanz commented at 5:07 pm on November 8, 2025: contributor
    Added REST API documentation and release notes & added a comment for rest_block (following @oren-z0 review).
  33. rest: allow reading partial block data from storage
    It will allow fetching specific transactions using an external index,
    following https://github.com/bitcoin/bitcoin/pull/32541#issuecomment-3267485313.
    e14650967d
  34. romanz force-pushed on Nov 8, 2025
  35. DrahtBot added the label CI failed on Nov 8, 2025
  36. DrahtBot commented at 5:14 pm on November 8, 2025: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/actions/runs/19196021604/job/54877130784 LLM reason (✨ experimental): Lint failure: release note snippets are in the wrong folder (doc_release_note_snippets detected; should be /doc/release-notes-.md).

    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.

  37. DrahtBot removed the label CI failed on Nov 8, 2025
  38. romanz requested review from oren-z0 on Nov 8, 2025

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-11-12 18:13 UTC

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