{
 "number": 34083,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/34083",
 "title": "Add initial vectorized chacha20 implementation for 2-3x speedup",
 "author": "theuni",
 "author_association": "MEMBER",
 "created_at": "2025-12-16T20:42:37Z",
 "updated_at": "2026-09-06T15:03:00Z",
 "age_days": 274,
 "draft": true,
 "labels": [],
 "milestone": null,
 "base": "master",
 "head_sha": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
 "head_ref": "chacha20-vectorized-initial",
 "head_repo": "theuni/bitcoin",
 "head_history": [
  {
   "t": "2026-08-21T21:36:13Z",
   "sha": "e3386b9d0d08ba963adc52e8ec484c63b663cf64"
  }
 ],
 "additions": 434,
 "deletions": 5,
 "changed_files": 5,
 "commit_count": 3,
 "size_bucket": "L",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "jonatack",
      "url": "https://github.com/bitcoin/bitcoin/pull/34083#issuecomment-5358697758"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "jonatack": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-20T16:21:31Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 1,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 3,
  "distinct_reviewers": [
   "ajtowns",
   "fanquake",
   "jonatack",
   "l0rinc",
   "maflcko",
   "sedited",
   "sipa"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-08-25T16:02:56Z",
  "last_reviewer_activity": "2026-08-30T03:53:40Z",
  "last_reviewer": "l0rinc",
  "author_silent_days": 23,
  "waiting_on_author_days": 18,
  "days_since_update": 11
 },
 "refs": {
  "mentioned": [
   286
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 286,
    "type": "issue",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "Static keypool (key cycling, dated files, isolated from config)"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/crypto/CMakeLists.txt",
  "src/crypto/chacha20.cpp",
  "src/crypto/chacha20_vec.cpp",
  "src/crypto/chacha20_vec.h",
  "src/crypto/chacha20_vec.ipp",
  "src/crypto/chacha20_vec_128impl.h",
  "src/crypto/chacha20_vec_base.cpp",
  "src/crypto/chacha_vec_impl.h"
 ],
 "body": "Exploit modern simd to calculate 2/4/6/8/16 states at a time depending on the size of the input.\n\nThis demonstrates a 2x speedup on x86-64 and 3x for arm+neon. Platforms which require runtime detection (avx2/avx512) improve performance even further, and will come as a follow-up.\n\nRather than hand-writing assembly or using arch-specific intrinsics, this is written using compiler built-ins understood by gcc and clang.\n\nIn practice (at least on x86_64 and armv8), the compilers are able to produce assembly that's not much worse than hand-written.\n\nThis means that every architecture can benefit from its own vectorized instructions without having to write/maintain an implementation for each one. But because each will vary in ability to exploit the parallelism, we allow (via ifdefs) each architecture to opt-out of some or all multi-state calculation at compile-time..\n\nHere, as a starting point, x86-64 and arm+neon have been defined based on local benchmarks.\n\nLocal profiling revealed that chacha20 accounts for a substantial amount of the network thread's time. It's not clear to me if speeding up chacha20 will improve network performance/latency, but it will definitely make it more efficient.\n\nThis is part 1 of a series of PR's for chacha20. I think it makes sense to take a look at the generic implementation and tune the architecture-specific defines for parallel blocks before adding the runtime-dependent platforms.\n\nMy WIP branch which includes avx2/avx512 can be seen here: https://github.com/theuni/bitcoin/commits/chacha20-vectorized/\n\nI've been hacking on this for quite a while, trying every imaginable tweak and comparing lots of resulting asm/ir. I'm happy to answer any questions about any choices made which aren't immediately obvious.\n\nEdit: some more impl details:\n\nI wrestled with gcc/clang a good bit, tweaking something and comparing the generated code output. A few things I found, which may explain some of the decisions I made:\n\n- gcc really wanted to inline some of the helpers, which comes at a very substantial performance cost due to register clobbering (and with avx2, `vzeroupper`). Hence, all helpers are decorated with `ALWAYS_INLINE`.\n- gcc/clang do well with the vec256 loads/stores with minimal fussing. Though loading each element with `[]` is clumsy and verbose, it avoids compiler-specific layout assumptions. Other things I tried (which produced the same asm):\n  - casting directly to `using unaligned_vec256 __attribute__((aligned (1))) = vec256`\n  - memcpy into ^^\n  - clang's `__builtin_masked_load`\n- Loop unrolling was hit-or-miss without `#pragma GCC unroll n`, and I tried to avoid macros for loops, hence the awkward recursive inline template loops. But in practice, I see those unrolled 100% of the time.\n- I used `std::get` in the helpers for some extra compile-time safety (this actually pointed out some off-by-one's that would've been annoying to track down)\n- I avoided using any lambdas or classes for fear of compilers missing obvious optimizations\n- All `vec256` are passed by reference to avoid an annoying clang warning about returning a vector changing the abi (this is specific to x86 when not compiling with `-avx`). Even though our functions are all inlined, I didn't see any harm in making that adjustment.",
 "commits": [
  {
   "sha": "8fb231205c40a8b6dbc6c78c9ac46f978e3962f4",
   "date": "2026-08-21T21:17:03Z",
   "message": "chacha20: move single-block crypt to inline helper function"
  },
  {
   "sha": "e3386b9d0d08ba963adc52e8ec484c63b663cf64",
   "date": "2026-08-21T21:17:29Z",
   "message": "chacha20: Add generic vectorized chacha20 implementation\n\nExploit modern simd to calculate 2/4/6/8/16 states at a time depending on the\nsize of the input.\n\nDemonstrates a 2x speedup on x86-64 and 3x for arm+neon. Platforms which\nrequire runtime detection (avx2/avx512) improve performance even further, and\nwill come as a follow-up.\n\nRather than hand-writing assembly or using arch-specific intrinsics, this is\nwritten using compiler built-ins understood by gcc and clang.\n\nIn practice (at least on x86_64 and armv8), the compilers are able to produce\nassembly that's not much worse than hand-written.\n\nThis means that every architecture can benefit from its own vectorized\ninstructions without having to write/maintain an implementation for each one.\nBut because each will vary in ability to exploit the parallelism, we allow\n(via ifdefs) each architecture to opt-out of some or all multi-state\ncalculation at compile-time..\n\nHere, as a starting point, x86-64 and arm+neon have been defined based on local\nbenchmarks.\n\nCo-authored-by: L\u0151rinc <pap.lorinc@gmail.com>"
  },
  {
   "sha": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "date": "2026-08-25T16:02:56Z",
   "message": "squashme: fixup namespaces and circular dependencies"
  }
 ],
 "timeline": [
  {
   "t": "2025-12-16T20:49:14Z",
   "kind": "comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "text": "Adding pings for a few people I've discussed this with: @sipa @ajtowns @l0rinc"
  },
  {
   "t": "2025-12-17T01:11:28Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_base.cpp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": null,
   "text": "Why separate this into an .ipp file if it's only included in a single .cpp file?"
  },
  {
   "t": "2025-12-17T01:39:15Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_base.cpp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": null,
   "text": "Using `#if !defined` for these seems old school. Why not static constexpr bools, and `if constexpr (..)`?\n\nWriting:\n\n```c++\n#if defined(__x86_64__) || defined(__amd64__)\n#  define CHACHA20_VEC_DISABLE_STATES_16 true\n#  define CHACHA20_VEC_DISABLE_STATES_8  true\n#  define CHACHA20_VEC_DISABLE_STATES_6  true\n#  define CHACHA20_VEC_DISABLE_STATES_4  false\n#  define CHACHA20_VEC_DISABLE_STATES_2  false\n#elif defined(__ARM_NEON)\n...\n\nstatic constexpr bool CHACHA20_VEC_ALL_MULTI_STATES_DISABLED =\n    CHACHA20_VEC_DISABLE_STATES_16 &&\n    CHACHA20_VEC_DISABLE_STATES_8 &&\n    CHACHA20_VEC_DISABLE_STATES_6 &&\n    CHACHA20_VEC_DISABLE_STATES_4 &&\n    CHACHA20_VEC_DISABLE_STATES_2;\n\n void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const std::array<uint32_t, 12>& input) noexcept\n {\n    if constexpr (CHACHA20_VEC_ALL_MULTI_STATES_DISABLED) return;\n...\n    if constexpr (!CHACHA20_VEC_DISABLE_STATES_16) {\n         while(in_bytes.size() >= CHACHA20_VEC_BLOCKLEN * 16) {\n...\n```\n\nseems to work right, and also seems like it avoids needing to put all the potentially unused code in a #if block."
  },
  {
   "t": "2025-12-17T02:03:33Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.ipp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": null,
   "text": "`static_assert(sizeof(vec256) == 32)` ? (Or `sizeof(vec256) == 32 || ALL_MULTI_STATES_DISABLED`)"
  },
  {
   "t": "2025-12-17T02:04:20Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": null,
   "text": "`if constexpr (ITER + 1 < I)`   (we have a space after `if constexpr` elsewhere)"
  },
  {
   "t": "2025-12-17T02:07:55Z",
   "kind": "review",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "3bddf59cd3f201ecf8d65bb1f6c0cde5c39595e9",
   "text": "Some nits. Approach looks very nice, and at least going by the bench results, gives a good improvement."
  },
  {
   "t": "2025-12-17T07:58:39Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_base.cpp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2625293528,
   "text": "Here's a branch that replaces the #defines and abstracts the recursive template stuff a bit more for your perusal https://github.com/ajtowns/bitcoin/commits/202512-pr34083-templates/"
  },
  {
   "t": "2025-12-17T08:11:01Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Looking forward to reviewing it - quick question before I do: was it tested on any big-endian systems which don't revert to non-vectorized run?"
  },
  {
   "t": "2025-12-17T08:48:01Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": null,
   "text": "My understanding is that you're inverting the logic here: instead of\n\n```c++\n#define QUARTERROUND(a,b,c,d) \\\n    X(a,b,d,16) X(c,d,b,12) X(a,d,b,8) X(c,d,b,7)\n```\n\nThis does the `X(a,b,d,16)` all four times via simd, then `X(c,d,b,12)` all four times, etc. And the simd part relies on the data for those four steps being exactly +4 units apart, which is why the shuffling and unshuffling is necessary for the second round. (Okay, not four times but eight times because each vec256 is two blocks)\n\nCould use a little more explanation in the comment I think? But makes sense to me."
  },
  {
   "t": "2025-12-17T08:58:20Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20.cpp",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "Overflow happens every 274GB I guess, which presumably isn't worth putting much effort in to optimising around."
  },
  {
   "t": "2025-12-17T09:24:53Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.ipp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": null,
   "text": "can we assume that all big endian systems have `__builtin_bswap32` available?\nhttps://github.com/bitcoin/bitcoin/blob/432b18ca8d0654318a8d882b28b20af2cb2d2e5d/src/compat/byteswap.h#L14-L16 indicates we could use our existing helpers here instead:\n```C++\nALWAYS_INLINE void vec_byteswap(vec256& vec)\n{\n    if constexpr (std::endian::native == std::endian::big) {\n        for (size_t i = 0; i < 8; ++i) {\n            vec[i] = internal_bswap_32(vec[i]);\n        }\n    }\n}\n```\nA small, fixed-size loop like this should be unrolled by every compiler, it's what we did in https://github.com/bitcoin/bitcoin/pull/31144/files#diff-f26d4597a7f5a5d4aa30053032d49427b402ef4fd7a1b3194cb75b2551d58ca4R48 as well.\n\n(nit: could you please reformat the patch, it differs slightly from how clang-format is set for new code)"
  },
  {
   "t": "2025-12-17T09:25:36Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": null,
   "text": "It seems to me the existing standard test vectors are too short (mostly < 128 bytes) to trigger the vectorized optimization - can we extend the tests to runa and compare the new specializations (could show as `skipped` when the given architecture isn't available locally)?"
  },
  {
   "t": "2025-12-17T14:33:11Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_base.cpp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": null,
   "text": "Forcing vectorized operations on an emulated big-endian system (otherwise it falls back to the scalar implementation due to the safety checks):\n\n```patch\ndiff --git a/src/crypto/chacha20_vec_base.cpp b/src/crypto/chacha20_vec_base.cpp\nindex 9fda9452a1..a2aa1e5552 100644\n--- a/src/crypto/chacha20_vec_base.cpp\n+++ b/src/crypto/chacha20_vec_base.cpp\n@@ -20,7 +20,7 @@\n #  define CHACHA20_VEC_DISABLE_STATES_8\n #  define CHACHA20_VEC_DISABLE_STATES_6\n #  define CHACHA20_VEC_DISABLE_STATES_4\n-#  define CHACHA20_VEC_DISABLE_STATES_2\n+//#  define CHACHA20_VEC_DISABLE_STATES_2\n #endif\n\n #include <crypto/chacha20_vec.ipp>\n```\n\nand running the `crypto_tests`:\n\n```bash\nbrew install podman pigz qemu\npodman machine init\npodman machine start\n\npodman run --platform linux/s390x -it --rm ubuntu:latest /bin/bash -c \\\n  'apt-get update && \\\n   DEBIAN_FRONTEND=noninteractive apt-get install -y \\\n   git build-essential cmake ccache pkg-config \\\n   libevent-dev libboost-dev libssl-dev libsqlite3-dev python3 && \\\n   git clone https://github.com/bitcoin/bitcoin.git && cd bitcoin && \\\n   git fetch origin pull/34083/head:chacha20-vec && git checkout chacha20-vec && \\\n   sed -i \"s/#  define CHACHA20_VEC_DISABLE_STATES_2/\\/\\/#  define CHACHA20_VEC_DISABLE_STATES_2/\" src/crypto/chacha20_vec_base.cpp && \\\n   cmake -B build -DBUILD_BENCH=OFF -DBUILD_GUI=OFF -DBUILD_DAEMON=OFF -DBUILD_TX=OFF -DENABLE_IPC=OFF -DENABLE_WALLET=OFF -DENABLE_ZMQ=OFF -DENABLE_UPNP=OFF -DENABLE_NATPMP=OFF && \\\n   cmake --build build --target test_bitcoin -j1 && \\ ./build/bin/test_bitcoin --run_test=crypto_tests'\n```\n\nWe're getting a lot of failures:\n```bash\nRunning 17 test cases...\n./test/crypto_tests.cpp(155): error: in \"crypto_tests/chacha20_testvector\": check hexout == HexStr(outres) has failed [a3fbf07df3fa2fde4f376ca23e82737041605d9f4f4f57bd8cff2c1d4b7955ec2a97948bd3722915c8f3d337f7d370050e9e96d647b7c39f56e031ca5eb6250d4042e02785ececfa4b4bb5e8ead0440e20b6e8db09d881a7c6132f420e52795042bdfa7773d8a9051447b3291ce1411c680465552aa6c405b7764d5e87bea85ad00f8449ed8f72d0d662ab052691ca66424bc86d2df80ea41f43abf937d3259dc4b2d0dfb48a6c9139ddd7f76966e928e635553ba76c5c879d7b35d49eb2e62b0871cdac638939e25e8a1e0ef9d5280fa8ca328b351c3c765989cbcf3daa8b6ccc3aaf9f3979c92b3720fc88dc95ed84a1be059c6499b9fda236e7e818b04b0bc39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e698ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221 != c2ece71ceded38c04f376ca225cc3d6b463409986f263e9db1994a204b6844ec6e9695cfc52b7003e3b6961ceac96a18138981cb4ee5919649b263d542b82b114a57f52d8ba2a2f4540af4f7f49f0b1072a7f9891b97ceb5c61c20420143685f169add2373c4b505122eda2f5df3535d555637680dc5a722b83209519ae7f147c11a9158f48479c9926ea7412ac69d6a584ac97768e412e1514fa7b737ce389dc4bbd9df9cc422b95ccfc5926171fe20e9284834a77646878d7a34c485b3e7300c3589a8448b3bc53b980c6bced42938afc1398c2f1a3a6c2887c5be68a18039cb2fba983271c1202a73af95c79ae29faafb40973694b4afa523f2ef13b84300c39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e698ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221]\n...\n./test/crypto_tests.cpp(185): error: in \"crypto_tests/chacha20_testvector\": check hexout == HexStr(outres) has failed [a3fbf07df3fa2fde4f376ca23e82737041605d9f4f4f57bd8cff2c1d4b7955ec2a97948bd3722915c8f3d337f7d370050e9e96d647b7c39f56e031ca5eb6250d4042e02785ececfa4b4bb5e8ead0440e20b6e8db09d881a7c6132f420e52795042bdfa7773d8a9051447b3291ce1411c680465552aa6c405b7764d5e87bea85ad00f8449ed8f72d0d662ab052691ca66424bc86d2df80ea41f43abf937d3259dc4b2d0dfb48a6c9139ddd7f76966e928e635553ba76c5c879d7b35d49eb2e62b0871cdac638939e25e8a1e0ef9d5280fa8ca328b351c3c765989cbcf3daa8b6ccc3aaf9f3979c92b3720fc88dc95ed84a1be059c6499b9fda236e7e818b04b0bc39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e698ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221 != a3fbf07df3fa2fde4f376ca23e82737041605d9f4f4f57bd8cff2c1d4b7955ec2a97948bd3722915c8f3d337f7d370050e9e96d647b7c39f56e031ca5eb6250d4a57f52d8ba2a2f4540af4f7f49f0b1072a7f9891b97ceb5c61c20420143685f169add2373c4b505122eda2f5df3535d555637680dc5a722b83209519ae7f147c11a9158f48479c9926ea7412ac69d6a584ac97768e412e1514fa7b737ce389dc4bbd9df9cc422b95ccfc5926171fe20e9284834a77646878d7a34c485b3e7300871cdac638939e25e8a1e0ef9d5280fa8ca328b351c3c765989cbcf3daa8b6ccc3aaf9f3979c92b3720fc88dc95ed84a1be059c6499b9fda236e7e818b04b0bc39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e698ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221]\n./test/crypto_tests.cpp(272): error: in \"crypto_tests/chacha20poly1305_testvectors\": check cipher == expected_cipher has failed\n...\n./test/crypto_tests.cpp(337): error: in \"crypto_tests/chacha20poly1305_testvectors\": check decipher == plain has failed\n\n*** 37 failures are detected in the test module \"Bitcoin Core Test Suite\"\n```\n\nThis appears to be an endianness issue in `vec_read_xor_write`: the current implementation swaps the result of the XOR, but on Big Endian systems we must swap the state `vec` *before* the XOR.\n\nChanging it to:\n\n```patch\ndiff --git a/src/crypto/chacha20_vec.ipp b/src/crypto/chacha20_vec.ipp\nindex 46a159ce01..cfc0535a92 100644\n--- a/src/crypto/chacha20_vec.ipp\n+++ b/src/crypto/chacha20_vec.ipp\n@@ -174,8 +174,9 @@ ALWAYS_INLINE void vec_read_xor_write(std::span<const std::byte, 32> in_bytes, s\n {\n     std::array<uint32_t, 8> temparr;\n     memcpy(temparr.data(), in_bytes.data(), in_bytes.size());\n-    vec256 tempvec = vec ^ (vec256){temparr[0], temparr[1], temparr[2], temparr[3], temparr[4], temparr[5], temparr[6], temparr[7]};\n+    vec256 tempvec = vec;\n     vec_byteswap(tempvec);\n+    tempvec ^= (vec256){temparr[0], temparr[1], temparr[2], temparr[3], temparr[4], temparr[5], temparr[6], temparr[7]};\n     temparr = {tempvec[0], tempvec[1], tempvec[2], tempvec[3], tempvec[4], tempvec[5], tempvec[6], tempvec[7]};\n     memcpy(out_bytes.data(), temparr.data(), out_bytes.size());\n }\n ```\n\nMakes it pass for me.\n\nWe should find a way to exercise this via CI and benchmarks, maybe similarly to `SHA256AutoDetect` in https://github.com/bitcoin/bitcoin/blob/bdb8eadcdc193f398ebad83911d3297b5257e721/src/crypto/sha256.cpp#L585-L690 which would enable us running benchmarks and tests selectively."
  },
  {
   "t": "2025-12-17T15:17:29Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": null,
   "text": "Hmmm, it seems to me we're doing heavy recursive templates here to unroll loops.\nMy understanding (and experience with the mentioned Obfuscation PR) is that modern compilers are good at unrolling fixed-bound loops.\nCan you please try if this also works and results in the same speedup?\n\n```suggestion\ntemplate <size_t BITS, size_t I>\nALWAYS_INLINE void arr_add_xor_rot(std::array<vec256, I>& arr0, const std::array<vec256, I>& arr1, std::array<vec256, I>& arr2)\n{\n    for (size_t i{0}; i < I; ++i) {\n        arr0[i] += arr1[i];\n        arr2[i] ^= arr0[i];\n        vec_rotl<BITS>(arr2[i]);\n    }\n}\n```\n(nit: the size is often `N` instead of `I`)"
  },
  {
   "t": "2025-12-17T15:20:22Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": null,
   "text": "Could we use https://github.com/bitcoin/bitcoin/blob/e16c22fe025f82166c7f3f15a37c96bf4a06e4cf/src/attributes.h#L19-L25 instead?"
  },
  {
   "t": "2025-12-17T15:25:27Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.ipp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": null,
   "text": "There's a lot of repetition here - could we extract that to an `ALWAYS_INLINE` lambda and have something like:\n```C++\n    if constexpr (ENABLE_16) process_blocks(16);\n    else if constexpr (ENABLE_8)  process_blocks(8);\n...\n```\n\nI haven't implemented it locally, maybe it's naive, let me know what you think."
  },
  {
   "t": "2025-12-17T15:26:05Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_base.cpp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2625293528,
   "text": "+1 for `if constexpr`, should simplify the code a lot (especially if we migrate from recursive templates as well)"
  },
  {
   "t": "2025-12-17T15:39:10Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.ipp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2625334750,
   "text": "I haven't used this before but the internets tells me we could use the attribute syntax here, something like:\n```suggestion\ntemplate <typename T, size_t N>\nusing VectorType [[gnu::vector_size(sizeof(T) * N)]] = T;\n\nusing vec256 = VectorType<uint32_t, 8>;\n```"
  },
  {
   "t": "2025-12-17T15:46:36Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "CHANGES_REQUESTED",
   "commit": "3bddf59cd3f201ecf8d65bb1f6c0cde5c39595e9",
   "text": "I went through the code roughly, I like that we're focusing on this and looking forward to specializing other parts of the code that are this critical (looking at you, SipHash!).\n\nMy biggest objection currently is that it's broken on big-endian systems - left a suggestion how to reproduce and fix it. This also reveals the lack of testing - we have to find a way to selectively enable and disable these optimizations to make sure we have tests that compare their outputs.\nI agree with AJ that we could modernize this a bit with `constexpr` and less general recursive template magic since C++20 allows us to use simple loops and `constexpr` conditions and lambdas - it could reduce a lot of duplication while maintaining performance.\nThere's also some repetition (e.g. `ALWAYS_INLINE` and `internal_bswap_32`), already defined elsewhere which I think we could use instead.\nWhich leads me to think we should extract the new primitives used here (`__builtin_shufflevector`, `vec_rotl`, `vec_byteswap`, `vec256`) to a reusable header - tested and benchmarked separately from the chacha work."
  },
  {
   "t": "2025-12-17T15:59:54Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": null,
   "text": "I believe it's possible to use compile-time loops here instead of recursive templates (if runtime loops don't get optimized sufficiently):\n\n```c++\n/** Store a vector in all array elements */\ntemplate <size_t I>\nALWAYS_INLINE void arr_set_vec256(std::array<vec256, I>& arr, const vec256& vec)\n{\n    [&]<size_t... ITER>(std::index_sequence<ITER...>) {\n        ((std::get<ITER>(arr) = vec),...);\n    }(std::make_index_sequence());\n}\n```"
  },
  {
   "t": "2025-12-17T16:10:37Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": 2627477691,
   "text": "Alternatively, with a compile-time loop:\n\n```c++\n/** Perform add/xor/rotate for the round function */\ntemplate <size_t BITS, size_t I>\nALWAYS_INLINE void arr_add_xor_rot(std::array<vec256, I>& arr0, const std::array<vec256, I>& arr1, std::array<vec256, I>& arr2)\n{\n    [&]<size_t... ITER>(std::index_sequence<ITER...>) {\n        ((\n            arr0[ITER] += arr1[ITER],\n            arr2[ITER] ^= arr0[ITER],\n            vec_rotl<BITS>(arr2[ITER])\n        ), ...);\n    }(std::make_index_sequence());\n}\n```"
  },
  {
   "t": "2025-12-17T16:14:47Z",
   "kind": "review_comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_base.cpp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2625248846,
   "text": "See branch here: https://github.com/theuni/bitcoin/commits/chacha20-vectorized/\n\nI decided to exclude the impls which require runtime detection from this PR, as I think how that should be done is a separate conversation.\n\nTo answer your question more specifically: some impls may require different compilation flags (`-mavx2`/`-mavx512vl`), which have to be in their own compilation units."
  },
  {
   "t": "2025-12-17T16:25:11Z",
   "kind": "review_comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_base.cpp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2625293528,
   "text": "` if constexpr` certainly makes more sense where possible. I'll have a look, thanks!"
  },
  {
   "t": "2025-12-17T17:03:37Z",
   "kind": "review_comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.ipp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2625334750,
   "text": "Because they're compiler-specific, the code makes no assumptions about the size/structure/alignment of vec256. The only accesses are via `operator[]`. So afaik, there's no need to check for this."
  },
  {
   "t": "2025-12-17T17:10:48Z",
   "kind": "review_comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20.cpp",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": 2626160716,
   "text": "This is actually doing the opposite.. it's keeping the vectorized impl from having to worry about this case. In the current code, we have:\n```c++\n++j12;\nif (!j12) ++j13;\n...\ninput[8] = j12;\ninput[9] = j13;\n```\n\nSo effectively `input[8]` and `input[9]` are treated as a single `uint64_t`. It's not possible to express \"cast to `uint64_t` elements and increment\" or \"increment and overflow over there\" with the vector extensions, so it turned out to be easier to just forbid the overflow cases from being vectorized at all."
  },
  {
   "t": "2025-12-17T17:12:11Z",
   "kind": "review_comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.ipp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2626253726,
   "text": "Thanks, yes, I meant to change that to `internal_bswap_32` before pushing. Will do."
  },
  {
   "t": "2025-12-17T17:12:41Z",
   "kind": "review_comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": 2626256140,
   "text": "Yep, will do."
  },
  {
   "t": "2025-12-17T17:17:27Z",
   "kind": "review_comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_base.cpp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2627310095,
   "text": "Thanks for catching this! I forgot to mention in the PR description that big-endian was best-effort and untested. I figured our c-i would catch any obvious bugs. Agree it's not great that it didn't :(\n\nThanks for the quick fix too :)"
  },
  {
   "t": "2025-12-17T17:22:33Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_base.cpp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2627310095,
   "text": "I had the same disappointment when implementing the obfuscation optimization. :)\n\n@maflcko do we have a big-endian nightly that supports vectorized operations? Would it have caught this?"
  },
  {
   "t": "2025-12-17T17:25:36Z",
   "kind": "review_comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": 2627477691,
   "text": "See updated title description where I touched on this.\n\nI found (with lots of experimentation here) that different compilers use complicated and unpredictable heuristics to decide whether or not to unroll loops. Even if an unroll-able loop is detected, unrolling may be skipped because of the function size (as is the case here because it's huge).\n\nSo, I'd like to not take chances on unrolling. That means one of:\n- Manual unrolling\n- Macro-based (as-in `REPEAT10()`)\n- A pragma\n- Recursive template based\n\n[c++26 introduces `template for`](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p1306r5.html), which is what we really want.\n\nI'm up for whichever of those is generally preferred, as long as it's explicit rather than implicit."
  },
  {
   "t": "2025-12-17T17:30:03Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": 2627477691,
   "text": "Is there any way we could help that would make you reconsider? I don't mind running the benchmarks on a few platforms, as long as we can have simpler code. The current template magic is not something I would like to see more of - modern C++20 should be able to handle the situations we have here. I don't mind experimenting with this if you don't want to."
  },
  {
   "t": "2025-12-17T17:41:07Z",
   "kind": "review_comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": 2627635541,
   "text": "I avoided lambdas as I've historically observed lots of optims being skipped (with clang, at least) when using them. But since you/@ajtowns/@l0rinc have all made the same comment, I'll play around and see if these indeed compile down to nothing as one would hope."
  },
  {
   "t": "2025-12-17T17:41:14Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": 2627477691,
   "text": "@theuni See my `std::make_index_sequence` based I demonstrated above. It's template based, but doesn't need recursion, and doesn't rely on compiler unrolling. It simply expands to an expression `(a[0] = v, a[1] = v, a[2] = v, ...)` e.g."
  },
  {
   "t": "2025-12-17T17:45:59Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": 2627477691,
   "text": "https://www.agner.org/optimize/optimizing_cpp.pdf contains a few useful examples:\n[quoted text omitted]\n\nSimilarly in https://en.algorithmica.org/hpc/simd/auto-vectorization useful hints:\n[quoted text omitted]"
  },
  {
   "t": "2025-12-17T17:48:28Z",
   "kind": "comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "text": "An additional note about the actual vectorizing algorithm itself...\n\nThere's a different (and arguably more obvious) algorithm that's possible when calculating exactly 8 states. Rather than loading each `vec256` with partial info from 2 states, it's possible to use 16 `vec256` where each vector contains 1 element for each of the 8 states. It looks like:\n\n```c++\nvec256 x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15;\n\nbroadcast(x0, 0x61707865);\nbroadcast(x1, 0x3320646e);\nbroadcast(x2, 0x79622d32);\nbroadcast(x3, 0x6b206574);\nbroadcast(x4, input[0]);\n...\nbroadcast(x15, input[11]);\n\nQUARTERROUND( x0, x4, x8,x12);\nQUARTERROUND( x1, x5, x9,x13);\n...\n\nvec256 j0, j1, j2, j3, ... j31;\nextract_column(x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, 0, j0, j1);\nextract_column(x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, 1, j2, j3);\n...\nextract_column(x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, 15, j30, j31);\n\nvec_read_xor_write(in_bytes, out_bytes, j0);\nvec_read_xor_write(in_bytes, out_bytes, j1);\n...\n\n```\n\nThe problem with this is the overhead of the transpose at the end. My experiments showed (on x86_64+avx2, at least) that this was actually slower than what's currently implemented. It's possible that this approach is better for other architectures, but I left it out of this PR to reduce the initial complexity.\n\nEdit: This is what the linux kernel does for x86_64+avx2. I grabbed their .S and hacked it into Core to benchmark, only to find that the impl here actually outperformed their hand-written asm. That was neat :)"
  },
  {
   "t": "2025-12-17T17:50:01Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_base.cpp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2627310095,
   "text": "See https://github.com/bitcoin/bitcoin/pull/33436, but it was slow despite having the gui and the fuzz tests disabled. I am running it as part of nightly, so any issues will be caught before a release, but I am not sure if catching everything in pull requests is possible.\n\nMaybe it is possible to split the task into two: One for a cross-compile, which should be faster than a \"native\" compile via qemu. And another to run the tests, similar to the windows-cross tests."
  },
  {
   "t": "2025-12-17T17:55:37Z",
   "kind": "review_comment",
   "who": "sipa",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": 2627635541,
   "text": "It can be done with a helper function too, but that's not as concise:\n\n```c++\ntemplate <size_t I, size_t... ITER>\nALWAYS_INLINE void arr_set_vec256_inner(std::array<vec256, I>& arr, const vec256& vec, std::index_sequence<ITER...>)\n{\n    ((std::get<ITER>(arr) = vec),...);\n}\n\n/** Store a vector in all array elements */\ntemplate <size_t I>\nALWAYS_INLINE void arr_set_vec256(std::array<vec256, I>& arr, const vec256& vec)\n{\n    arr_set_vec256_inner(arr, vec, std::make_index_sequence());\n}\n```"
  },
  {
   "t": "2025-12-17T21:46:10Z",
   "kind": "comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "text": "Ok, after some experimenting, I am very hesitant to add the helpers as suggested above. For example, consider @ajtowns's seemingly innocuous Repeat function:\n```c++\ntemplate <size_t REPS, typename Fn>\nALWAYS_INLINE void Repeat(Fn&& fn)\n{\n    if constexpr (REPS > 0) {\n        fn();\n        Repeat<REPS-1>(std::forward<Fn>(fn));\n    }\n}\n```\n\nThis causes a 10% slowdown on this branch, but with avx2 the performance is abysmal:\n\nBaseline(master):\n```\n|                1.38 |      723,275,584.44 | `CHACHA20_1MB`\n```\nThis PR:\n```\n|                0.71 |    1,408,751,284.34 | `CHACHA20_1MB`\n```\n[202512-pr34083-templates](https://github.com/ajtowns/bitcoin/commits/202512-pr34083-templates/)\n```\n|                0.79 |    1,258,748,451.87 | `CHACHA20_1MB`\n```\n\nThis PR + [avx2 commits](https://github.com/theuni/bitcoin/commits/chacha20-vectorized/):\n```\n|                0.50 |    1,988,653,922.83 |  `CHACHA20_1MB`\n```\n\n[202512-pr34083-templates](https://github.com/ajtowns/bitcoin/commits/202512-pr34083-templates/) + [avx2 commits](https://github.com/theuni/bitcoin/commits/chacha20-vectorized/):\n```\n|                1.34 |      748,148,324.75 | `CHACHA20_1MB`\n```\n\nThe problem in AJ's branch that some functions end up un-inlined. For clang, it's `doubleround`. For gcc it's `arr_read_xor_write` and `doubleround`.\n\nBoth are fixed with:\n```diff\ndiff --git a/src/crypto/chacha20_vec.ipp b/src/crypto/chacha20_vec.ipp\nindex 344c68259a5..16d7da45633 100644\n--- a/src/crypto/chacha20_vec.ipp\n+++ b/src/crypto/chacha20_vec.ipp\n@@ -164 +164 @@ public:\n-        Repeat<10>([&]() {\n+        Repeat<10>([&]() __attribute__ ((always_inline)) {\n```\n\nThat means, to be safe, every lambda would need that attribute, which is pretty ugly and easy to forget. So I think my aversion to lambdas in this code was somewhat justified.\n\nFurther, clang's [inlining docs](https://clang.llvm.org/docs/analyzer/developer-docs/IPA.html) state that variadic functions aren't inlined. Experimenting now, that's obviously not true as of v21-git. But clearly it was in the recent past.\n\nSo.. I'd _really_ prefer to keep things as simple here as possible. I know the recursive template functions aren't exactly pretty, but I don't think they're _THAT_ bad? Plus, with c++26, it all goes away in favor of `template for` :)"
  },
  {
   "t": "2025-12-18T01:22:05Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.ipp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2625334750,
   "text": "Sorry, I meant just so you get an error if you're using a compiler ignores the `__attribute__` entirely but still passes the `#if` in _base.cpp."
  },
  {
   "t": "2025-12-18T01:23:16Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20.cpp",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": 2626160716,
   "text": "Yeah, I was thinking about whether the code should just do the non-vectorized stuff to get past the overflow then immediately go back to vectorizing, rather than waiting for the next call. I think what you've got makes sense."
  },
  {
   "t": "2025-12-18T01:38:13Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nCould we get some comments in the code as to compiler versions (and architecture) that failed to be efficient enough with lambdas, to hopefully avoid future people modernizing the code without checking that these problems have gone away? Presumably will be a long time before we can modernize to `template for` (which doesn't even seem to be on [cppref's compiler support page](https://en.cppreference.com/w/cpp/compiler_support.html) yet?)...\n\n(Only thing that I do think is \"that bad\" about the recursive templates is using `ITER` and `I` -- `i` should be the loop variable, not the size!)"
  },
  {
   "t": "2025-12-18T01:58:30Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": 2627635541,
   "text": "Writing this as:\n\n```c++\n    template<size_t... ITER> using iseq = std::index_sequence<ITER>;\n    static constexpr ISEQ = std::make_index_sequence();\n\n    template<size_t... ITER>\n    ALWAYS_INLINE void arr_set_vec256(iseq<ITER>, std::array<vec256, I>& arr, const vec256& vec)\n    {\n        ((std::get<ITER>(arr) = vec), ...);\n    }\n\n   ...\n        arr_set_vec256(ISEQ, arr0, num256);\n```\n\ndoesn't seem too bad, and avoids lambdas? Possibly still a bit clumsy if you can't capture `I` by putting everything in a class? Might still be a bit clumsy for add_xor_rot."
  },
  {
   "t": "2025-12-18T06:13:49Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_base.cpp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2627310095,
   "text": "Actually, I ran the CI config locally, but it didn't catch this, as the platform opts out. The CI doesn't run the `   sed -i \"s/#  define CHACHA20_VEC_DISABLE_STATES_2/\\/\\/#  define CHACHA20_VEC_DISABLE_STATES_2/\" src/crypto/chacha20_vec_base.cpp && \\` portion, so it would not have caught this, even if the task was run."
  },
  {
   "t": "2025-12-18T11:47:22Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_base.cpp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2627310095,
   "text": "Thanks for checking @maflcko, that's why I asked.\nThe vectorized operations are obviously supported, but for some reason were not triggered for me either.\n@theuni, is this just an emulation anomaly or we were just too cautious?\nI understdood that @achow101 has access to real big-endian power9 machine that we might be able to test this on when it's ready."
  },
  {
   "t": "2025-12-18T15:22:20Z",
   "kind": "review_comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_base.cpp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2627310095,
   "text": "@l0rinc As the code is written at the moment, all platforms must opt-in to vectorization as opposed to opting out. I'm not sure that's the best approach, but I figured that was a reasonable starting point.\n\nMy reasoning for that was: consider non-x86, non-arm+neon platforms. For the most part, I'm assuming they're under-powered. Enabling (for example) 4x blocks/sec for mipsel would probably cause a drastic slowdown. Obviously there are lots of other powerful platforms, but I figured those would be added over time.\n\nSo if there's a big-endian architecture (or more ideally, a specific instruction set ala `__ARM_NEON`) that demonstrates a performance gain from calculating multiple states at once, it should be added here.\n\nOf course, we could go the opposite direction and opt all platforms IN to all states, and instead opt-out the slow ones.\n\ntl;dr: If we want a big-endian platform to be supported, we need to opt one in or provide an override."
  },
  {
   "t": "2025-12-18T15:52:48Z",
   "kind": "comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nSure, I can definitely add some more context and clean up the wonky variable names.\n\nAnother brain dump after thinking about all this some more last night:\n\n1. I don't think lambdas specifically are the problem, more specifically, the issue is: can the compiler infer enough about the \"callback\" function to inline it?\n\nThere are a few considerations there. @sipa's `make_index_sequence` suggestion is executed immediately, so I imagine that's more likely to be inlined than AJ's `Repeat`. But still, from a compiler's POV, if it's evaluating: \"here's a function call without `ALWAYS_INLINE` and my function is already huge, should I exclude it from inlining?\", imo it'd be reasonable for it to conclude \"yes\".\n\nSo to be safe, whatever we do, I think we should strive to annotate all functions. And imo, annotating a bunch of lambdas with an inline attribute feels weird.\n\n(as an aside: There's also the `flatten` attribute, which could be added to `multi_block_crypt` as a belt-and-suspenders)\n\nThe `index_sequence` trick is neat. If that ends up looking cleaner/more obvious than the recursive iteration, that works for me. I'll play around with it.\n\n2. Not all loops have to be unrolled.\n\nIn my testing, loop unrolling always showed slightly better performance (presumably because calculating multiple blocks is otherwise branch-free), but it's not nearly as performance-critical as the inlining. For example, skipping unrolling of `doubleround` leads to _much_ smaller code, which I imagine could end up being faster on some architectures.\n\n[quoted text omitted]\nMy primary goal with this code (and hopefully setting a precedent for other multi-arch simd code... poly1305 is next ;) is to be as explicit to the compiler about what we want as possible. Ideally as portably as possible. So if we want our functions inlined or loops unrolled, we should attempt to communicate those things opposed to leaving them implicit. Unfortunately, modern c++ doesn't have ways to express either of those yet, but we can make our intentions clear enough."
  },
  {
   "t": "2025-12-19T09:38:43Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nYou could `#define AI __attribute__((always_inline))`, then you'd just be adding `AI` to all the lambdas, which, if nothing else, would be very modern?\n\n[quoted text omitted]\nThe above was kind-of a joke, but, perhaps you could combine it with a clang-tidy plugin that lets CI check that all lambdas in a particular namespace are annotated in that way? That might be both clear to compilers and reasonably friendly to human authors/reviewers?\n\n(Explicit recursive templates and prohibiting lambdas are fine by me though; that's still a big step up from inline asm)"
  },
  {
   "t": "2025-12-22T08:18:17Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "I have measured its effect on IBD and happy to say it produces a measurable speedup.\n\nIBD | 926619 blocks | dbcache 450 | i7-hdd | x86_64 | Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz | 8 cores | 62Gi RAM | ext4 | HDD\n\n```\nCOMMITS=\"938d7aacabd0bb3784bb3e529b1ed06bb2891864 3bddf59cd3f201ecf8d65bb1f6c0cde5c39595e9\"; \\\nSTOP=926619; DBCACHE=450; \\\nCC=gcc; CXX=g++; \\\nBASE_DIR=\"/mnt/my_storage\"; DATA_DIR=\"$BASE_DIR/BitcoinData\"; LOG_DIR=\"$BASE_DIR/logs\"; \\\n(echo \"\"; for c in $COMMITS; do git fetch -q origin $c && git log -1 --pretty='%h %s' $c || exit 1; done) && \\\n(echo \"\" && echo \"IBD | ${STOP} blocks | dbcache ${DBCACHE} | $(hostname) | $(uname -m) | $(lscpu | grep 'Model name' | head -1 | cut -d: -f2 | xargs) | $(nproc) cores | $(free -h | awk '/^Mem:/{print $2}') RAM | $(df -T $BASE_DIR | awk 'NR==2{print $2}') | $(lsblk -no ROTA $(df --output=source $BASE_DIR | tail -1) | grep -q 0 && echo SSD || echo HDD)\"; echo \"\") &&\\\nhyperfine \\\n  --sort command \\\n  --runs 2 \\\n  --export-json \"$BASE_DIR/ibd-$(sed -E 's/(\\w{8})\\w+ ?/\\1-/g;s/-$//'<<<\"$COMMITS\")-$STOP-$DBCACHE-$CC.json\" \\\n  --parameter-list COMMIT ${COMMITS// /,} \\\n  --prepare \"killall -9 bitcoind 2>/dev/null; rm -rf $DATA_DIR/*; git checkout {COMMIT}; git clean -fxd; git reset --hard && \\\n    cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo && ninja -C build bitcoind -j2 && \\\n    ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=1 -printtoconsole=0; sleep 20\" \\\n  --conclude \"cp $DATA_DIR/debug.log $LOG_DIR/debug-{COMMIT}-$(date +%s).log && \\\n             grep -q 'height=0' $DATA_DIR/debug.log && grep -q 'Disabling script verification at block #1' $DATA_DIR/debug.log && grep -q 'height=$STOP' $DATA_DIR/debug.log\" \\\n  \"COMPILER=$CC ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=$DBCACHE -blocksonly -printtoconsole=0\"\n```\n\n[quoted text omitted]\n```\nBenchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=926619 -dbcache=450 -blocksonly -printtoconsole=0 (COMMIT = 938d7aacabd0bb3784bb3e529b1ed06bb2891864)\n  Time (mean \u00b1 \u03c3):     45198.232 s \u00b1 539.231 s    [User: 57185.689 s, System: 4367.881 s]\n  Range (min \u2026 max):   44816.939 s \u2026 45579.526 s    2 runs\n\nBenchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=926619 -dbcache=450 -blocksonly -printtoconsole=0 (COMMIT = 3bddf59cd3f201ecf8d65bb1f6c0cde5c39595e9)\n  Time (mean \u00b1 \u03c3):     43699.641 s \u00b1 25.532 s    [User: 57610.736 s, System: 4058.824 s]\n  Range (min \u2026 max):   43681.587 s \u2026 43717.695 s    2 runs\n\nRelative speed comparison\n        1.03 \u00b1  0.01  COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=926619 -dbcache=450 -blocksonly -printtoconsole=0 (COMMIT = 938d7aacabd0bb3784bb3e529b1ed06bb2891864)\n        1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=926619 -dbcache=450 -blocksonly -printtoconsole=0 (COMMIT = 3bddf59cd3f201ecf8d65bb1f6c0cde5c39595e9)\n```"
  },
  {
   "t": "2025-12-24T07:50:01Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nPresumably that indicates this IBD run is compute bound (vs disk or network), and that ChaCha20 is using up maybe 7% of CPU time (or 7% of a core when we're bottlenecked on something single-threaded) prior to this PR. That seems like a lot? Is this due to FastRandomContext, or something else? Or is it just that obfuscating all the block data is a large component of IBD CPU currently? This seems a bit surprising to me."
  },
  {
   "t": "2025-12-24T23:02:17Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.ipp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": null,
   "text": "I think this function should get a short description too. Wasn't immediately clear to me that it attempts to peel off larger blocks first before potentially finishing with a few smaller blocks."
  },
  {
   "t": "2025-12-24T23:04:50Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": 2627635541,
   "text": "Is there a particular reason for the aversion to the current approach? It seems easier to read and probably also to debug to me."
  },
  {
   "t": "2025-12-28T13:49:29Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": 2626127018,
   "text": "Makes sense to me too, but I'm not sure why doing it this way is preferable. Intuitively I would have expected that the vectors hold the same word spread over multiple blocks. But if I understand your approach here, we have multiple words over two blocks. Is it just easier to express the quarter rounds in this way?"
  },
  {
   "t": "2026-01-06T21:18:08Z",
   "kind": "comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nNot sure if you missed this in the description, but that's exactly what this is trying to improve:\n[quoted text omitted]\n\nIBD would indeed be slowed by Chacha20 via single-threaded bip324 handling on the network thread. [While profiling my POC multi-process net binary](https://github.com/bitcoin-core/libmultiprocess/issues/215) I observed `ChaCha20Aligned::Crypt()` accounting for 30% of the net thread's cpu time.\n\nSo it's not surprising to me at all that 3x'ing that function (it's only 2x here, avx2/avx512 improve performance even more) speeds up IBD."
  },
  {
   "t": "2026-01-13T19:34:47Z",
   "kind": "comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "text": "I finally managed to track down the gcc slowdown that @l0rinc mentioned during last week's IRC meeting. The culprit was [this gcc bug](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=107563#c14). Thankfully, it's easily worked around by simply not calling the guilty builtin.\n\nAlso pushed @l0rinc's fix for big-endian.\n\nNow that gcc/clang are more on par and the impl seems feasible again, I'll address the other feedback."
  },
  {
   "t": "2026-05-04T17:42:18Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.h",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nWe could avoid a copy by making it a `span` instead:\n```suggestion\n    void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, 12> input) noexcept;\n```"
  },
  {
   "t": "2026-05-04T17:43:54Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20.cpp",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": 2626160716,
   "text": "If you decide to keep it, consider subtracting from max instead:\n```suggestion\n    const bool overflow = blocks > std::numeric_limits<uint32_t>::max() - input[8];\n```"
  },
  {
   "t": "2026-05-04T17:45:32Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.ipp",
   "commit": "3bddf59cd3f201ecf8d65bb1f6c0cde5c39595e9",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nWe might have mentioned this before but we can reduce duplication by extracting this to something like:\n```C++\ntemplate <size_t STATES>\nALWAYS_INLINE void process_blocks(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const vec256& state0, const vec256& state1, vec256& state2)\n{\n    static constexpr vec256 increment = (vec256){STATES, 0, 0, 0, STATES, 0, 0, 0};\n    while (in_bytes.size() >= CHACHA20_VEC_BLOCKLEN * STATES) {\n        multi_block_crypt<STATES>(in_bytes, out_bytes, state0, state1, state2);\n        state2 += increment;\n        in_bytes = in_bytes.subspan(CHACHA20_VEC_BLOCKLEN * STATES);\n        out_bytes = out_bytes.subspan(CHACHA20_VEC_BLOCKLEN * STATES);\n    }\n}\n```\nand use it as e.g.\n```C++\n    process_blocks<16>(in_bytes, out_bytes, state0, state1, state2);\n```\n\nLocal benchmarks indicate it retains the performance."
  },
  {
   "t": "2026-05-04T17:50:15Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.ipp",
   "commit": "3bddf59cd3f201ecf8d65bb1f6c0cde5c39595e9",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nCould we simplify this by using `memcpy` directly between byte spans and `vec256` instead, something like:\n```suggestion\n    vec256 tempvec;\n    memcpy(&tempvec, in_bytes.data(), sizeof(tempvec));\n    vec_byteswap(tempvec);\n    tempvec ^= vec;\n    vec_byteswap(tempvec);\n    memcpy(out_bytes.data(), &tempvec, sizeof(tempvec));\n```"
  },
  {
   "t": "2026-05-04T17:52:41Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "CHANGES_REQUESTED",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "text": "I have remeasured it on a few platforms - on RPI5 this is a serious slowdown compared to before:\n\nMachine | Baseline | Final | Speedup\n-- | -- | -- | --\nMac M4 Max | 1062 MB/s | 2172 MB/s | 2.05x\numbrel N150 (GCC 12.2) | 116 MB/s | 272 MB/s | 2.34x\nRPi5 (GCC 15.0.1) | 406 MB/s | 207 MB/s | 0.51x \u26a0\ufe0f\nRPi5 (Clang 22.0.0) | 452 MB/s | 558 MB/s | 1.24x\n\nRPi5 with GCC shows a regression: the generic vectorized implementation is ~2x slower than scalar on ARM64 with GCC, while Clang handles it fine.\nThe \"gcc/clang fix\" commit helps x86 (umbrel) but doesn't fix the ARM64/GCC issue.\nI haven't investigated the source of the problem, just identified it.\n\n----\n\nMac M4 - AppleClang 21.0.0.21000099\n\n```\nfor commit in 224120bf1299392deaa59ab71c895a1e6264f205 b9300cc696135d1ff31ffbdf639dc0f99167d49c 96f741441180d14df0519caa3f7c73f818a12dbf 26e9c7b588fcbdc956c4e2b241f4ef7d121d3d79 e6ec033f175031f72e23253bd0821f6fc6d353b2 6db8cf0e28bbaef24cb9e1e3f0c59f118a25619c 63a99f2b31efde3db91349bbf13f3c243c453084 e82c752615badf8a811846122c2c370d645385e9 62a8c487da9721fd1149dc3fefa7fb11292370bf 332fa6e26f293af58fad83ccc896291a68650fae 548791e2b58fb65a52159a18f3765613e11422a3; do \\\n    git fetch origin $commit >/dev/null 2>&1 && git checkout $commit >/dev/null 2>&1 && echo \"\" && git log -1 --pretty='%h %s' && \\\n    rm -rfd build >/dev/null 2>&1 && cmake -B build -DBUILD_BENCH=ON -DCMAKE_BUILD_TYPE=Release >/dev/null 2>&1 && \\\n    cmake --build build -j$(nproc) >/dev/null 2>&1 && \\\n    for _ in $(seq 1); do \\\n      sleep 5; \\\n      sudo taskpolicy -t 5 -l 5 nice -n -20 ./build/bin/bench_bitcoin -filter='CHACHA20_.*' -min-time=10000; \\\n    done; \\\ndone\n\n224120bf12 Merge bitcoin/bitcoin#32394: net: make m_nodes_mutex non-recursive\n\n|             ns/byte |              byte/s |    err% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------:|:----------\n|                0.94 |    1,061,878,902.65 |    0.4% |     11.00 | `CHACHA20_1MB`\n|                0.95 |    1,057,294,303.75 |    0.3% |     10.96 | `CHACHA20_256BYTES`\n|                0.96 |    1,037,370,700.06 |    0.5% |     10.52 | `CHACHA20_64BYTES`\n\nb9300cc696 chacha20: move single-block crypt to inline helper function\n\n|             ns/byte |              byte/s |    err% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------:|:----------\n|                0.88 |    1,131,925,309.10 |    0.3% |     10.95 | `CHACHA20_1MB`\n|                0.89 |    1,123,374,575.47 |    0.4% |     10.89 | `CHACHA20_256BYTES`\n|                0.92 |    1,086,410,862.30 |    0.2% |     11.00 | `CHACHA20_64BYTES`\n\n96f7414411 chacha20: Add generic vectorized chacha20 implementation\n\n|             ns/byte |              byte/s |    err% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------:|:----------\n|                0.46 |    2,190,867,321.66 |    0.0% |     11.01 | `CHACHA20_1MB`\n|                0.60 |    1,664,336,473.57 |    0.1% |     10.99 | `CHACHA20_256BYTES`\n|                0.94 |    1,063,169,379.64 |    0.2% |     10.99 | `CHACHA20_64BYTES`\n\n26e9c7b588 squashme: fix vectorized chacha20 on big endian\n\n|             ns/byte |              byte/s |    err% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------:|:----------\n|                0.46 |    2,175,076,479.08 |    0.1% |     11.00 | `CHACHA20_1MB`\n|                0.61 |    1,644,558,660.17 |    0.1% |     10.94 | `CHACHA20_256BYTES`\n|                0.95 |    1,055,198,073.31 |    0.3% |     10.53 | `CHACHA20_64BYTES`\n\ne6ec033f17 squashme: fix main performance discrepancy between clang and gcc\n\n|             ns/byte |              byte/s |    err% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------:|:----------\n|                0.46 |    2,172,474,228.31 |    0.1% |     11.00 | `CHACHA20_1MB`\n|                0.61 |    1,641,693,934.28 |    0.3% |     10.97 | `CHACHA20_256BYTES`\n|                0.95 |    1,051,868,628.18 |    0.3% |     10.54 | `CHACHA20_64BYTES`\n```\n\nUmbrel\n\n```\nfor compiler in gcc; do \\\n  if [ \"$compiler\" = \"gcc\" ]; then CC=gcc; CXX=g++; COMP_VER=$(gcc -dumpfullversion); \\\n  else CC=clang; CXX=clang++; COMP_VER=$(clang -dumpversion); fi && \\\n  echo \"> Compiler: $compiler $COMP_VER\" && \\\n  for commit in 224120bf1299392deaa59ab71c895a1e6264f205 b9300cc696135d1ff31ffbdf639dc0f99167d49c 96f741441180d14df0519caa3f7c73f818a12dbf 26e9c7b588fcbdc956c4e2b241f4ef7d121d3d79 e6ec033f175031f72e23253bd0821f6fc6d353b2 6db8cf0e28bbaef24cb9e1e3f0c59f118a25619c 63a99f2b31efde3db91349bbf13f3c243c453084 e82c752615badf8a811846122c2c370d645385e9 62a8c487da9721fd1149dc3fefa7fb11292370bf 332fa6e26f293af58fad83ccc896291a68650fae 548791e2b58fb65a52159a18f3765613e11422a3; do \\\n    git fetch origin $commit >/dev/null 2>&1 && git checkout $commit >/dev/null 2>&1 && echo \"\" && git log -1 --pretty='%h %s' && \\\n    rm -rf build >/dev/null 2>&1 && cmake -B build -DBUILD_BENCH=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=$CC -DCMAKE_CXX_COMPILER=$CXX >/dev/null 2>&1 && \\\n    cmake --build build -j$(nproc) >/dev/null 2>&1 && \\\n    for i in 1; do \\\n      build/bin/bench_bitcoin -filter='CHACHA20_.*' -min-time=10000; \\\n    done; \\\n  done; \\\ndone\n```\n\nUmbrel: gcc 12.2.0\n\n```\n224120bf12 Merge bitcoin/bitcoin#32394: net: make m_nodes_mutex non-recursive\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                8.60 |      116,311,101.69 |    0.0% |           16.55 |            6.92 |  2.391 |           0.02 |    0.0% |     10.86 | `CHACHA20_1MB`\n|                8.76 |      114,180,117.53 |    0.0% |           17.21 |            7.05 |  2.441 |           0.08 |    0.0% |     10.97 | `CHACHA20_256BYTES`\n|                9.26 |      108,047,834.96 |    0.0% |           19.22 |            7.45 |  2.578 |           0.27 |    0.0% |     11.00 | `CHACHA20_64BYTES`\n\nb9300cc696 chacha20: move single-block crypt to inline helper function\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                8.67 |      115,349,945.71 |    0.0% |           16.75 |            6.98 |  2.401 |           0.02 |    0.0% |     10.87 | `CHACHA20_1MB`\n|                8.81 |      113,493,232.74 |    0.0% |           17.31 |            7.10 |  2.440 |           0.05 |    0.0% |     11.00 | `CHACHA20_256BYTES`\n|                9.22 |      108,508,333.73 |    0.0% |           19.00 |            7.42 |  2.560 |           0.17 |    0.0% |     11.01 | `CHACHA20_64BYTES`\n\n96f7414411 chacha20: Add generic vectorized chacha20 implementation\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                7.27 |      137,465,451.07 |    0.0% |           12.54 |            5.85 |  2.142 |           0.00 |    0.0% |     10.70 | `CHACHA20_1MB`\n|                7.37 |      135,758,621.02 |    0.0% |           13.04 |            5.93 |  2.199 |           0.07 |    0.0% |     11.00 | `CHACHA20_256BYTES`\n|                9.33 |      107,202,828.01 |    0.1% |           19.25 |            7.51 |  2.563 |           0.22 |    0.0% |     11.00 | `CHACHA20_64BYTES`\n\n26e9c7b588 squashme: fix vectorized chacha20 on big endian\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                7.28 |      137,438,778.33 |    0.0% |           12.54 |            5.86 |  2.141 |           0.00 |    0.0% |     10.69 | `CHACHA20_1MB`\n|                7.37 |      135,749,652.15 |    0.0% |           13.04 |            5.93 |  2.199 |           0.07 |    0.0% |     11.00 | `CHACHA20_256BYTES`\n|                9.32 |      107,248,535.46 |    0.0% |           19.25 |            7.51 |  2.563 |           0.22 |    0.0% |     11.00 | `CHACHA20_64BYTES`\n\ne6ec033f17 squashme: fix main performance discrepancy between clang and gcc\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                3.68 |      271,912,991.35 |    0.0% |            9.79 |            2.96 |  3.310 |           0.00 |    0.0% |     11.01 | `CHACHA20_1MB`\n|                3.80 |      263,471,169.28 |    0.0% |           10.29 |            3.06 |  3.366 |           0.07 |    0.0% |     11.00 | `CHACHA20_256BYTES`\n|                9.32 |      107,266,696.30 |    0.0% |           19.25 |            7.51 |  2.564 |           0.22 |    0.0% |     11.00 | `CHACHA20_64BYTES`\n```\n\nRpi5\n\n```\nrpi@rpi5-8:/mnt/my_storage/bitcoin$ for compiler in gcc clang; do \\\n  if [ \"$compiler\" = \"gcc\" ]; then CC=gcc; CXX=g++; COMP_VER=$(gcc -dumpfullversion); \\\n  else CC=clang; CXX=clang++; COMP_VER=$(clang -dumpversion); fi && \\\n  echo \"> Compiler: $compiler $COMP_VER\" && \\\n  for commit in 224120bf1299392deaa59ab71c895a1e6264f205 b9300cc696135d1ff31ffbdf639dc0f99167d49c 96f741441180d14df0519caa3f7c73f818a12dbf 26e9c7b588fcbdc956c4e2b241f4ef7d121d3d79 e6ec033f175031f72e23253bd0821f6fc6d353b2 6db8cf0e28bbaef24cb9e1e3f0c59f118a25619c 63a99f2b31efde3db91349bbf13f3c243c453084 e82c752615badf8a811846122c2c370d645385e9 62a8c487da9721fd1149dc3fefa7fb11292370bf 332fa6e26f293af58fad83ccc896291a68650fae 548791e2b58fb65a52159a18f3765613e11422a3; do \\\n    git fetch origin $commit >/dev/null 2>&1 && git checkout $commit >/dev/null 2>&1 && echo \"\" && git log -1 --pretty='%h %s' && \\\n    rm -rf build >/dev/null 2>&1 && cmake -B build -DBUILD_BENCH=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=$CC -DCMAKE_CXX_COMPILER=$CXX >/dev/null 2>&1 && \\\n    cmake --build build -j$(nproc) >/dev/null 2>&1 && \\\n    for i in 1; do \\\n      build/bin/bench_bitcoin -filter='CHACHA20_.*' -min-time=10000; \\\n    done; \\\n  done; \\\ndone\n```\n\nRpi5: gcc 15.0.1\n\n```\n224120bf12 Merge bitcoin/bitcoin#32394: net: make m_nodes_mutex non-recursive\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                2.46 |      406,186,773.08 |    0.0% |           15.56 |            5.89 |  2.640 |           0.02 |    0.0% |     11.00 | `CHACHA20_1MB`\n|                2.50 |      400,125,462.96 |    0.0% |           16.14 |            5.99 |  2.697 |           0.07 |    0.0% |     10.59 | `CHACHA20_256BYTES`\n|                2.70 |      370,016,682.88 |    0.0% |           17.88 |            6.47 |  2.762 |           0.25 |    0.0% |     11.00 | `CHACHA20_64BYTES`\n\nb9300cc696 chacha20: move single-block crypt to inline helper function\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                2.32 |      430,122,901.72 |    0.0% |           15.63 |            5.57 |  2.807 |           0.02 |    0.0% |     11.01 | `CHACHA20_1MB`\n|                2.43 |      411,502,538.79 |    0.2% |           16.11 |            5.82 |  2.767 |           0.06 |    0.0% |     10.55 | `CHACHA20_256BYTES`\n|                2.58 |      387,118,423.10 |    0.1% |           17.55 |            6.19 |  2.836 |           0.19 |    0.0% |     11.01 | `CHACHA20_64BYTES`\n\n96f7414411 chacha20: Add generic vectorized chacha20 implementation\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                4.83 |      206,987,012.46 |    0.0% |           23.35 |           11.56 |  2.021 |           0.00 |    0.1% |     10.98 | `CHACHA20_1MB`\n|                4.81 |      207,928,148.69 |    0.0% |           20.59 |           11.52 |  1.788 |           0.07 |    0.0% |     11.00 | `CHACHA20_256BYTES`\n|                2.71 |      368,891,391.01 |    0.0% |           17.84 |            6.49 |  2.749 |           0.19 |    0.0% |     11.00 | `CHACHA20_64BYTES`\n\n26e9c7b588 squashme: fix vectorized chacha20 on big endian\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                4.84 |      206,725,965.93 |    0.0% |           23.35 |           11.57 |  2.018 |           0.00 |    0.4% |     11.00 | `CHACHA20_1MB`\n|                4.80 |      208,151,217.86 |    0.0% |           20.59 |           11.50 |  1.790 |           0.07 |    0.0% |     11.01 | `CHACHA20_256BYTES`\n|                2.72 |      367,823,395.01 |    0.0% |           17.84 |            6.51 |  2.741 |           0.19 |    0.0% |     11.00 | `CHACHA20_64BYTES`\n\ne6ec033f17 squashme: fix main performance discrepancy between clang and gcc\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                4.83 |      207,011,227.50 |    0.0% |           23.29 |           11.56 |  2.015 |           0.00 |    0.1% |     11.00 | `CHACHA20_1MB`\n|                4.84 |      206,631,508.34 |    0.0% |           20.58 |           11.59 |  1.776 |           0.07 |    0.0% |     11.00 | `CHACHA20_256BYTES`\n|                2.71 |      368,486,078.94 |    0.0% |           17.84 |            6.50 |  2.745 |           0.19 |    0.0% |     11.00 | `CHACHA20_64BYTES`\n\n```\n\nRpi5: clang 22.0.0\n\n```\n224120bf12 Merge bitcoin/bitcoin#32394: net: make m_nodes_mutex non-recursive\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                2.21 |      451,646,217.56 |    0.0% |           15.72 |            5.30 |  2.967 |           0.03 |    0.0% |     11.00 | `CHACHA20_1MB`\n|                2.30 |      435,574,764.05 |    0.1% |           16.31 |            5.50 |  2.967 |           0.08 |    0.0% |     11.00 | `CHACHA20_256BYTES`\n|                2.55 |      391,818,167.07 |    0.0% |           18.09 |            6.11 |  2.960 |           0.23 |    0.0% |     11.00 | `CHACHA20_64BYTES`\n\nb9300cc696 chacha20: move single-block crypt to inline helper function\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                2.21 |      451,882,020.43 |    0.0% |           15.72 |            5.30 |  2.967 |           0.03 |    0.0% |     11.00 | `CHACHA20_1MB`\n|                2.30 |      435,590,027.21 |    0.1% |           16.31 |            5.50 |  2.968 |           0.08 |    0.0% |     11.00 | `CHACHA20_256BYTES`\n|                2.55 |      391,773,300.32 |    0.0% |           18.09 |            6.11 |  2.960 |           0.23 |    0.0% |     11.00 | `CHACHA20_64BYTES`\n\n96f7414411 chacha20: Add generic vectorized chacha20 implementation\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                1.79 |      558,716,231.71 |    0.0% |            9.60 |            4.28 |  2.243 |           0.00 |    0.1% |     11.01 | `CHACHA20_1MB`\n|                1.65 |      607,517,921.76 |    0.0% |            6.55 |            3.94 |  1.662 |           0.10 |    0.0% |     11.00 | `CHACHA20_256BYTES`\n|                2.60 |      384,629,496.56 |    0.0% |           18.30 |            6.23 |  2.938 |           0.28 |    0.0% |     11.00 | `CHACHA20_64BYTES`\n\n26e9c7b588 squashme: fix vectorized chacha20 on big endian\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                1.79 |      558,241,984.97 |    0.0% |            9.60 |            4.28 |  2.242 |           0.00 |    0.1% |     11.00 | `CHACHA20_1MB`\n|                1.64 |      609,910,639.38 |    0.0% |            6.55 |            3.93 |  1.668 |           0.10 |    0.0% |     11.00 | `CHACHA20_256BYTES`\n|                2.58 |      386,939,531.73 |    0.0% |           18.30 |            6.19 |  2.956 |           0.28 |    0.0% |     11.00 | `CHACHA20_64BYTES`\n\ne6ec033f17 squashme: fix main performance discrepancy between clang and gcc\n\n|             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark\n|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------\n|                1.79 |      558,279,486.98 |    0.0% |            9.60 |            4.28 |  2.242 |           0.00 |    0.1% |     11.00 | `CHACHA20_1MB`\n|                1.64 |      609,989,533.96 |    0.0% |            6.55 |            3.93 |  1.669 |           0.10 |    0.0% |     11.00 | `CHACHA20_256BYTES`\n|                2.59 |      386,421,109.02 |    0.0% |           18.30 |            6.20 |  2.952 |           0.28 |    0.0% |     11.00 | `CHACHA20_64BYTES`\n```"
  },
  {
   "t": "2026-05-15T20:19:21Z",
   "kind": "comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "text": "@l0rinc Thanks for the benchmarks! Those were very helpful.\n\nI haven't gone through all of the comments here yet, since fixing the gcc regression was a blocker for everything else.\n\nI tracked down the root cause of the gcc slowdown compared to clang: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=125303\n\ntl;dr: clang gracefully uses 128bit simd when 256bit isn't available, gcc doesn't (yet).\n\nTo work around that, I refactored to allow either 128bit or 256bit depending on the architecture. This works great. With the new code, 256bit operations only give a sliiight speedup (using avx2) over the 128bit ones. So I think the 128bit simd approach is very reasonable.\n\ngcc is now in the same ballpark as clang, also showing a 2x-4x speedup on the hardware that I've tested.\n\n@l0rinc: If you'd like to bench the new approach, see the branch here: https://github.com/theuni/bitcoin/commits/chacha20-vectorized-128bit/\n\nI'll work on cleaning that up next week."
  },
  {
   "t": "2026-08-14T15:24:22Z",
   "kind": "comment",
   "who": "fanquake",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nAny chance you still want to clean that new branch up/rebase and push it up here?"
  },
  {
   "t": "2026-08-17T21:30:52Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "@theuni, if you're busy with other stuff, I don't mind taking over this change, I can probably push an updated version this week."
  },
  {
   "t": "2026-08-17T21:34:30Z",
   "kind": "comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "text": "Thanks for the pings. I can get to this this week. ~I'm planning to revert back to the 256bit impl and enable it for platforms which support that. By targeting avx2 and arm64, that should cover most users. Then in the future when gcc has caught up with clang, we can enable it unconditionally.~\n\nNevermind that plan. I misremembered the state of gcc+arm64. See the below comment for my updated proposal."
  },
  {
   "t": "2026-08-18T23:19:11Z",
   "kind": "comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "text": "@l0rinc I pushed up 2 big/messy commits. The first reworks the design to be _much_ less hacky. It does away with nearly all of the include weirdness and ifdef mess.\n\nThe second introduces a pluggable vector interface, and adds a 128bit implementation. This works around gcc's vectorizer's limitations and should fix the performance regressions you pointed out. It should now give a good speedup on x86_64 and arm64 without any slowdowns. This is a sacrifice in speed for clang (which happily breaks up 256bit vector operations per-platform as necessary/expected) and for gcc targets with 256bit operations available (like avx2).\n\nI chose this solution because, while not optimal, it at least gives a nice speedup (1.5x-2x) for the most widely used platforms. Once we get it in, we can look at adding an additional 256bit implementation and the necessary logic to decide at compile-time which should be used. In the future, once gcc is fixed and available enough, we can drop the 128bit one entirely.\n\nThe code still needs lots of documentation and small cleanups. The commit series really doesn't make sense anymore, it should essentially all be squashed down into a single commit. I pushed early mainly for the sake of concept ACKs and updated benchmarking. I'll continue working on documentation and nit fixups. We're down to the wire for feature-freeze and it may be too late now, but maybe we can get it squeezed in :)"
  },
  {
   "t": "2026-08-19T07:26:33Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Thanks @theuni, I also experimented with https://github.com/l0rinc/bitcoin/pull/286 as a slightly simpler alternative to this implementation (used a few AI cycles to strip it down this much), using one 128-bit SIMD path in `chacha20.cpp` for 4-block groups and 2-block horizontal remainders instead of a pluggable multi-file backend.\nIt was also slightly faster on the tested x86-64 and ARM64 platforms, so it might offer some useful ideas here.\n\nRaw GCC and Clang measurements and diffs\n\n**Intel i9-9900K (x86-64) results**\n\n| Benchmark | GCC yours ns/B | GCC l0rinc ns/B | GCC winner | Clang yours ns/B | Clang l0rinc ns/B | Clang winner |\n|:---|---:|---:|:---|---:|---:|:---|\n| CHACHA20_64BYTES | 2.05 | 1.79 | l0rinc by 14.5% | 1.85 | 1.83 | l0rinc by 1.1% |\n| CHACHA20_256BYTES | 0.98 | 0.81 | l0rinc by 21.0% | 0.89 | 0.82 | l0rinc by 8.5% |\n| CHACHA20_1MB | 0.91 | 0.81 | l0rinc by 12.3% | 0.83 | 0.81 | l0rinc by 2.5% |\n| FSCHACHA20POLY1305_64BYTES | 5.41 | 5.16 | l0rinc by 4.8% | 5.41 | 5.38 | l0rinc by 0.6% |\n| FSCHACHA20POLY1305_256BYTES | 2.38 | 2.23 | l0rinc by 6.7% | 2.28 | 2.21 | l0rinc by 3.2% |\n| FSCHACHA20POLY1305_1MB | 1.69 | 1.58 | l0rinc by 7.0% | 1.51 | 1.49 | l0rinc by 1.3% |\n\n---\n\n**4\u00d7 Cortex-A76 (ARM64) results**\n\n| Benchmark | GCC yours ns/B | GCC l0rinc ns/B | GCC winner | Clang yours ns/B | Clang l0rinc ns/B | Clang winner |\n|:---|---:|---:|:---|---:|---:|:---|\n| CHACHA20_64BYTES | 2.726 | 2.768 | yours by 1.5% | 2.593 | 2.631 | yours by 1.5% |\n| CHACHA20_128BYTES | 2.854 | 2.685 | l0rinc by 6.3% | 2.655 | 2.613 | l0rinc by 1.6% |\n| CHACHA20_192BYTES | 2.771 | 2.625 | l0rinc by 5.5% | 2.528 | 2.548 | yours by 0.8% |\n| CHACHA20_256BYTES | 1.696 | 1.439 | l0rinc by 17.9% | 1.733 | 1.671 | l0rinc by 3.7% |\n| CHACHA20_1MB | 1.654 | 1.426 | l0rinc by 16.0% | 1.629 | 1.601 | l0rinc by 1.8% |\n| FSCHACHA20POLY1305_64BYTES | 7.694 | 7.683 | l0rinc by 0.2% | 7.473 | 7.545 | yours by 1.0% |\n| FSCHACHA20POLY1305_256BYTES | 3.544 | 3.272 | l0rinc by 8.3% | 3.641 | 3.606 | l0rinc by 1.0% |\n| FSCHACHA20POLY1305_1MB | 2.447 | 2.224 | l0rinc by 10.0% | 2.570 | 2.544 | l0rinc by 1.0% |\n| FSCHACHA20POLY1305_BIP324_318BYTES | 4.754 | 4.697 | l0rinc by 1.2% | 4.606 | 4.616 | yours by 0.2% |\n| FSCHACHA20POLY1305_BIP324_319BYTES | 3.691 | 3.508 | l0rinc by 5.2% | 3.827 | 3.841 | yours by 0.4% |\n| FSCHACHA20POLY1305_BIP324_320BYTES | 4.250 | 4.080 | l0rinc by 4.2% | 4.382 | 4.411 | yours by 0.7% |"
  },
  {
   "t": "2026-08-20T16:13:29Z",
   "kind": "comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "text": "@l0rinc Thanks! I spent all day yesterday trying to grok how that actually works.\n\nTurns out there are some really interesting algorithmic improvements to the 4block version that I finally understand and have reproduced and experimented with. I'm working on a cleaned up version now.\n\ntl;dr: It has to do a painful matrix transmutation because of the layout, but uses out-of-order writes to minimize spilling. It's a cool trick :)"
  },
  {
   "t": "2026-08-20T16:21:31Z",
   "kind": "comment",
   "who": "jonatack",
   "assoc": "MEMBER",
   "text": "Concept ACK, good work."
  },
  {
   "t": "2026-08-21T21:36:13Z",
   "kind": "force_push",
   "who": "theuni",
   "commit": "e3386b9d0d08ba963adc52e8ec484c63b663cf64"
  },
  {
   "t": "2026-08-21T21:36:49Z",
   "kind": "comment",
   "who": "theuni",
   "assoc": "MEMBER",
   "text": "@l0rinc I spent quite a bit of time this week testing/comparing the horizontal layout approach with the vertical one. Ultimately I've concluded that the horizontal approach is the way to go. It's possible to eek out a _tiny_ bit of extra performance using the 4-state vertical layout, but that comes at the cost of still having to carry implementations for the other state counts. The tricks used in your branch are very cool, but I don't think it's worth the cost of keeping two implementations.\n\nI just pushed a simpler 128bit implementation that uses some of your suggestions. Dropped the vector abstraction stuff. I am able to match your benchmark numbers for all platforms, while shaving off an additional ~40% from arm64+clang on my m1. Untested on arm64+gcc though, fingers crossed there are no surprises there.\n\nI went ahead and nuked the history, rebased, and added you as a co-author while I was at it.\n\nComments and commit messages haven't been updated yet as this has been in heavy flux. Assuming there are no longer any obvious problems remaining, I can start getting it cleaned up (and passing c-i)."
  },
  {
   "t": "2026-08-21T21:41:58Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Thank @theuni, I'll review this during the weekend!\nNote that the CI indicates a dependency-cycle regression was introduced here."
  },
  {
   "t": "2026-08-29T20:29:24Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_128impl.h",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "We're taking 16 bytes per row, the comment should be updated\n```suggestion\n/** XOR 16 input bytes with one state row and write them without assuming alignment or vec128's memory layout */\n```"
  },
  {
   "t": "2026-08-29T20:30:40Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_128impl.h",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "```suggestion\n/** Write each 64-byte state in row order */\n```"
  },
  {
   "t": "2026-08-29T20:31:16Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_128impl.h",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "`nums256` is easy to mistake for the vector width here.\n\n```suggestion\n    static constexpr vec128 constants{0x61707865, 0x3320646e, 0x79622d32, 0x6b206574};\n```"
  },
  {
   "t": "2026-08-29T20:42:34Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_128impl.h",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "Is there a specific reason for redefining `ALWAYS_INLINE` here?\nThe other crypto backends already use (`sha256_*.cpp`, `siphash.h`) the one from `attributes.h`, could we do that here, too?"
  },
  {
   "t": "2026-08-29T20:57:38Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.h",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "Now that we're relying on `__builtin_shufflevector` after the GCC codegen issues (and since we only enable this on `x86_64`, `amd64`, and `aarch64`), should we guard the feature macros before using them?\n```suggestion\n#if defined(__has_attribute) && defined(__has_builtin)\n  #if __has_attribute(vector_size) && __has_builtin(__builtin_shufflevector) && (defined(__x86_64__) || defined(__amd64__) || defined(__aarch64__))\n    #define ENABLE_CHACHA20_VEC 1\n  #endif\n#endif\n```\n\nThe dispatcher then needs neither a separate target guard nor an x86-named boolean whose alternative implicitly means AArch64.\n\nlimit ChaCha20 vector targets\n\n```patch\ndiff --git a/src/crypto/chacha20_vec.cpp b/src/crypto/chacha20_vec.cpp\nindex e79f98b6ad..80f03f99ed 100644\n--- a/src/crypto/chacha20_vec.cpp\n+++ b/src/crypto/chacha20_vec.cpp\n@@ -9,35 +9,18 @@\n\n #include <cassert>\n\n-#if defined(__x86_64__) || defined(__amd64__)\n-static constexpr bool target_x86_64 = true;\n-#else\n-static constexpr bool target_x86_64 = false;\n-#endif\n-\n-#if defined(__aarch64__)\n-static constexpr bool target_arm64 = true;\n-#else\n-static constexpr bool target_arm64 = false;\n-#endif\n-\n-static constexpr bool use_vectorized = target_x86_64 || target_arm64;\n-\n namespace chacha20_vec {\n\n static_assert(BLOCKLEN == chacha20_vec128::BLOCKLEN);\n\n void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const std::array<uint32_t, 12>& input) noexcept\n {\n-    if constexpr (!use_vectorized) {\n-        return;\n-    }\n     assert(in_bytes.size() == out_bytes.size());\n     chacha20_vec128::ChaCha20Vectorized crypter(input);\n\n     while(in_bytes.size() >= BLOCKLEN) {\n         size_t blocks = out_bytes.size() / BLOCKLEN;\n-        if constexpr(target_x86_64) {\n+        if constexpr(TARGET == VectorTarget::X86_64) {\n             // 4 is faster than 3 + 1\n             // 4 + 4 is faster than 3 + 3 + 2\n             if  (blocks == 8 || blocks == 4) {\n@@ -49,7 +32,7 @@ void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<s\n             } else {\n                 crypter.CryptStates<3>(in_bytes, out_bytes);\n             }\n-        } else if constexpr (target_arm64) {\n+        } else if constexpr (TARGET == VectorTarget::AARCH64) {\n             // 15 is faster than 8 + 4 + 1 + 1 + 1\n             // 12 is faster than 8 + 4\n             if (blocks == 15 ) {\ndiff --git a/src/crypto/chacha20_vec.h b/src/crypto/chacha20_vec.h\nindex 456b1864bd..c0c172c0d8 100644\n--- a/src/crypto/chacha20_vec.h\n+++ b/src/crypto/chacha20_vec.h\n@@ -10,19 +10,30 @@\n #include <cstddef>\n #include <span>\n\n-#ifdef __has_attribute\n-  #if __has_attribute(vector_size)\n+#if defined(__has_attribute) && defined(__has_builtin)\n+  #if __has_attribute(vector_size) && __has_builtin(__builtin_shufflevector) && (defined(__x86_64__) || defined(__amd64__) || defined(__aarch64__))\n     #define ENABLE_CHACHA20_VEC 1\n   #endif\n #endif\n\n #ifdef ENABLE_CHACHA20_VEC\n\n-namespace chacha20_vec\n-{\n-    static constexpr size_t BLOCKLEN = 64;\n-    void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const std::array<uint32_t, 12>& input) noexcept;\n-}\n+namespace chacha20_vec {\n+static constexpr uint16_t BLOCKLEN{64};\n+\n+enum class VectorTarget {\n+    X86_64,\n+    AARCH64,\n+};\n+\n+#if defined(__x86_64__) || defined(__amd64__)\n+constexpr VectorTarget TARGET{VectorTarget::X86_64};\n+#elif defined(__aarch64__)\n+constexpr VectorTarget TARGET{VectorTarget::AARCH64};\n+#endif\n+\n+void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const std::array<uint32_t, 12>& input) noexcept;\n+} // namespace chacha20_vec\n\n #endif // ENABLE_CHACHA20_VEC\n```"
  },
  {
   "t": "2026-08-29T21:50:45Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.h",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": 3183361357,
   "text": "The vector backend already advances the remaining input and output spans, so the wrapper and extracted scalar helper are unnecessary.\n\nCould we pass the state as a fixed-extent span, dispatch vector groups at the start of `ChaCha20Aligned::Crypt`, and let its existing scalar loop process the remainder?\n\nsimplify `ChaCha20Aligned::Crypt`\n\n```patch\ndiff --git a/src/crypto/chacha20.cpp b/src/crypto/chacha20.cpp\nindex b7500237a5..891a157d57 100644\n--- a/src/crypto/chacha20.cpp\n+++ b/src/crypto/chacha20.cpp\n@@ -170,13 +170,11 @@ static inline void chacha20_crypt(std::span<const std::byte> in_bytes, std::span\n     std::byte* c = out_bytes.data();\n     size_t blocks = out_bytes.size() / ChaCha20Aligned::BLOCKLEN;\n     assert(blocks * ChaCha20Aligned::BLOCKLEN == out_bytes.size());\n-\n+    if (!blocks) return;\n\n     uint32_t x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15;\n     uint32_t j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15;\n\n-    if (!blocks) return;\n-\n     j4 = input[0];\n     j5 = input[1];\n     j6 = input[2];\n@@ -288,20 +286,14 @@ static inline void chacha20_crypt(std::span<const std::byte> in_bytes, std::span\n\n inline void ChaCha20Aligned::Crypt(std::span<const std::byte> in_bytes, std::span<std::byte> out_bytes) noexcept\n {\n-    assert(in_bytes.size() == out_bytes.size());\n-    size_t blocks = out_bytes.size() / ChaCha20Aligned::BLOCKLEN;\n-    assert(blocks * ChaCha20Aligned::BLOCKLEN == out_bytes.size());\n #ifdef ENABLE_CHACHA20_VEC\n     // The vectorized implementation cannot increment the first nonce word\n+    const size_t blocks{out_bytes.size() / BLOCKLEN};\n     assert(blocks < std::numeric_limits<uint32_t>::max() - input[8]);\n-    const auto state = std::to_array(input);\n-    chacha20_vec::chacha20_crypt_vectorized(in_bytes, out_bytes, state);\n-    const size_t blocks_written = blocks - (out_bytes.size() / ChaCha20Aligned::BLOCKLEN);\n-    input[8] += blocks_written;\n+    chacha20_vec::chacha20_crypt_vectorized(in_bytes, out_bytes, input);\n+    input[8] += blocks - out_bytes.size() / BLOCKLEN;\n #endif\n-    if (in_bytes.size()) {\n-        chacha20_crypt(in_bytes, out_bytes, input);\n-    }\n+    chacha20_crypt(in_bytes, out_bytes, input);\n }\n\n void ChaCha20::Keystream(std::span<std::byte> out) noexcept\ndiff --git a/src/crypto/chacha20_vec.cpp b/src/crypto/chacha20_vec.cpp\nindex 80f03f99ed..080eaa59e6 100644\n--- a/src/crypto/chacha20_vec.cpp\n+++ b/src/crypto/chacha20_vec.cpp\n@@ -13,7 +13,7 @@ namespace chacha20_vec {\n\n static_assert(BLOCKLEN == chacha20_vec128::BLOCKLEN);\n\n-void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const std::array<uint32_t, 12>& input) noexcept\n+void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept\n {\n     assert(in_bytes.size() == out_bytes.size());\n     chacha20_vec128::ChaCha20Vectorized crypter(input);\ndiff --git a/src/crypto/chacha20_vec.h b/src/crypto/chacha20_vec.h\nindex c0c172c0d8..9b58dd6d1c 100644\n--- a/src/crypto/chacha20_vec.h\n+++ b/src/crypto/chacha20_vec.h\n@@ -5,7 +5,6 @@\n #ifndef BITCOIN_CRYPTO_CHACHA20_VEC_H\n #define BITCOIN_CRYPTO_CHACHA20_VEC_H\n\n-#include <array>\n #include <cstdint>\n #include <cstddef>\n #include <span>\n@@ -20,6 +19,7 @@\n\n namespace chacha20_vec {\n static constexpr uint16_t BLOCKLEN{64};\n+static constexpr uint16_t STATE_WORDS{12};\n\n enum class VectorTarget {\n     X86_64,\n@@ -32,7 +32,7 @@ constexpr VectorTarget TARGET{VectorTarget::X86_64};\n constexpr VectorTarget TARGET{VectorTarget::AARCH64};\n #endif\n\n-void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const std::array<uint32_t, 12>& input) noexcept;\n+void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept;\n } // namespace chacha20_vec\n\n #endif // ENABLE_CHACHA20_VEC\ndiff --git a/src/crypto/chacha20_vec_128impl.h b/src/crypto/chacha20_vec_128impl.h\nindex 27f2d41b9f..fd876d3c09 100644\n--- a/src/crypto/chacha20_vec_128impl.h\n+++ b/src/crypto/chacha20_vec_128impl.h\n@@ -249,7 +249,7 @@ class ChaCha20Vectorized\n     const vec128 state1;\n     vec128 state2;\n public:\n-    ALWAYS_INLINE ChaCha20Vectorized(const std::array<uint32_t, 12>& input) noexcept\n+    ALWAYS_INLINE ChaCha20Vectorized(std::span<const uint32_t, chacha20_vec::STATE_WORDS> input) noexcept\n         : state0((vec128){input[0], input[1], input[2], input[3]})\n         , state1((vec128){input[4], input[5], input[6], input[7]})\n         , state2((vec128){input[8], input[9], input[10], input[11]})\n```"
  },
  {
   "t": "2026-08-29T21:54:10Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.cpp",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "I find it hard to understand when the same method has mutually exclusive implementations.\nUsually we could extract the parts to dedicated methods with well known boundaries and the top method could simply be responsible for dispatching to the appropriate impl.\n\nHere the x86-64 and AArch64 dispatchers are long `if`/`else if` chains inside `chacha20_crypt_vectorized`, so the common entry point owns both target selection and every grouping rule. Some cases only emerge from branch order, such as 8 blocks reaching the AArch64 fallback because no exact case matches first.\n\nCould we move each target loop into a named helper, express exact group counts with `switch`, and leave the entry point only to select one constexpr target?\n\nsplit ChaCha20 target dispatch\n\n```patch\ndiff --git a/src/crypto/chacha20_vec.cpp b/src/crypto/chacha20_vec.cpp\nindex 080eaa59e6..6214c726f7 100644\n--- a/src/crypto/chacha20_vec.cpp\n+++ b/src/crypto/chacha20_vec.cpp\n@@ -13,59 +13,66 @@ namespace chacha20_vec {\n\n static_assert(BLOCKLEN == chacha20_vec128::BLOCKLEN);\n\n-void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept\n+namespace {\n+\n+[[maybe_unused]] ALWAYS_INLINE void CryptX86_64(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept\n {\n     assert(in_bytes.size() == out_bytes.size());\n-    chacha20_vec128::ChaCha20Vectorized crypter(input);\n+    chacha20_vec128::ChaCha20Vectorized crypter{input};\n\n-    while(in_bytes.size() >= BLOCKLEN) {\n-        size_t blocks = out_bytes.size() / BLOCKLEN;\n-        if constexpr(TARGET == VectorTarget::X86_64) {\n-            // 4 is faster than 3 + 1\n-            // 4 + 4 is faster than 3 + 3 + 2\n-            if  (blocks == 8 || blocks == 4) {\n-                crypter.CryptStates<4>(in_bytes, out_bytes);\n-            } else if (blocks == 2) {\n-                crypter.CryptStates<2>(in_bytes, out_bytes);\n-            } else if (blocks == 1) {\n-                crypter.CryptStates<1>(in_bytes, out_bytes);\n-            } else {\n-                crypter.CryptStates<3>(in_bytes, out_bytes);\n-            }\n-        } else if constexpr (TARGET == VectorTarget::AARCH64) {\n-            // 15 is faster than 8 + 4 + 1 + 1 + 1\n-            // 12 is faster than 8 + 4\n-            if (blocks == 15 ) {\n-                crypter.CryptStates<15>(in_bytes, out_bytes);\n-            } else if (blocks == 14) {\n-                crypter.CryptStates<14>(in_bytes, out_bytes);\n-            } else if (blocks == 13) {\n-                crypter.CryptStates<13>(in_bytes, out_bytes);\n-            } else if (blocks == 12) {\n-                crypter.CryptStates<12>(in_bytes, out_bytes);\n-            } else if (blocks == 11) {\n-                crypter.CryptStates<11>(in_bytes, out_bytes);\n-            } else if (blocks == 10) {\n-                crypter.CryptStates<10>(in_bytes, out_bytes);\n-            } else if (blocks == 9) {\n-                crypter.CryptStates<9>(in_bytes, out_bytes);\n-            } else if (blocks == 7) {\n-                crypter.CryptStates<7>(in_bytes, out_bytes);\n-            } else if (blocks == 6) {\n-                crypter.CryptStates<6>(in_bytes, out_bytes);\n-            } else if (blocks == 5) {\n-                crypter.CryptStates<5>(in_bytes, out_bytes);\n-            } else if (blocks == 4) {\n-                crypter.CryptStates<4>(in_bytes, out_bytes);\n-            } else if  (blocks >= 8 ) {\n-                crypter.CryptStates<8>(in_bytes, out_bytes);\n-            } else {\n-                break;\n-            }\n+    while (in_bytes.size() >= BLOCKLEN) {\n+        const size_t blocks = out_bytes.size() / BLOCKLEN;\n+        // 4 is faster than 3 + 1\n+        // 4 + 4 is faster than 3 + 3 + 2\n+        switch (blocks) {\n+        case 1: crypter.CryptStates<1>(in_bytes, out_bytes); break;\n+        case 2: crypter.CryptStates<2>(in_bytes, out_bytes); break;\n+        default: crypter.CryptStates<3>(in_bytes, out_bytes); break;\n+        case 4:\n+        case 8: crypter.CryptStates<4>(in_bytes, out_bytes); break;\n         }\n     }\n }\n\n+[[maybe_unused]] ALWAYS_INLINE void CryptAArch64(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept\n+{\n+    assert(in_bytes.size() == out_bytes.size());\n+    chacha20_vec128::ChaCha20Vectorized crypter{input};\n+\n+    // 15 is faster than 8 + 4 + 1 + 1 + 1\n+    // 12 is faster than 8 + 4\n+    while (in_bytes.size() >= BLOCKLEN) {\n+        const size_t blocks = out_bytes.size() / BLOCKLEN;\n+        if (blocks < 4) break;\n+        switch (blocks) {\n+        case 3: crypter.CryptStates<3>(in_bytes, out_bytes); break;\n+        case 4: crypter.CryptStates<4>(in_bytes, out_bytes); break;\n+        case 5: crypter.CryptStates<5>(in_bytes, out_bytes); break;\n+        case 6: crypter.CryptStates<6>(in_bytes, out_bytes); break;\n+        case 7: crypter.CryptStates<7>(in_bytes, out_bytes); break;\n+        default: crypter.CryptStates<8>(in_bytes, out_bytes); break;\n+        case 9: crypter.CryptStates<9>(in_bytes, out_bytes); break;\n+        case 10: crypter.CryptStates<10>(in_bytes, out_bytes); break;\n+        case 11: crypter.CryptStates<11>(in_bytes, out_bytes); break;\n+        case 12: crypter.CryptStates<12>(in_bytes, out_bytes); break;\n+        case 13: crypter.CryptStates<13>(in_bytes, out_bytes); break;\n+        case 14: crypter.CryptStates<14>(in_bytes, out_bytes); break;\n+        case 15: crypter.CryptStates<15>(in_bytes, out_bytes); break;\n+        }\n+    }\n+}\n+\n+} // namespace\n+\n+void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept\n+{\n+    if constexpr (TARGET == VectorTarget::X86_64) {\n+        CryptX86_64(in_bytes, out_bytes, input);\n+    } else if constexpr (TARGET == VectorTarget::AARCH64) {\n+        CryptAArch64(in_bytes, out_bytes, input);\n+    }\n+}\n+\n } // namespace chacha20_vec\n\n #endif // ENABLE_CHACHA20_VEC\ndiff --git a/src/crypto/chacha20_vec.h b/src/crypto/chacha20_vec.h\nindex 9b58dd6d1c..d190f45bbb 100644\n--- a/src/crypto/chacha20_vec.h\n+++ b/src/crypto/chacha20_vec.h\n@@ -31,6 +31,7 @@ constexpr VectorTarget TARGET{VectorTarget::X86_64};\n #elif defined(__aarch64__)\n constexpr VectorTarget TARGET{VectorTarget::AARCH64};\n #endif\n+constexpr uint16_t MIN_BLOCKS{TARGET == VectorTarget::AARCH64 ? 3 : 1}; // Shorter AArch64 inputs are faster in scalar code\n\n void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept;\n } // namespace chacha20_vec\n```"
  },
  {
   "t": "2026-08-29T23:14:09Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_128impl.h",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "Now that we have `vec_add_xor_rot`, could we reuse the single-vector helper in the array recursion?\n\nreuse ChaCha20 vector round helper\n\n```patch\ndiff --git a/src/crypto/chacha20_vec_128impl.h b/src/crypto/chacha20_vec_128impl.h\nindex 3d207697f0..9342c78b6d 100644\n--- a/src/crypto/chacha20_vec_128impl.h\n+++ b/src/crypto/chacha20_vec_128impl.h\n@@ -100,13 +100,7 @@ ALWAYS_INLINE void vec_add_xor_rot(vec128& x, const vec128& y, vec128& z)\n template <size_t BITS, size_t I, size_t ITER = 0>\n ALWAYS_INLINE void arr_add_xor_rot(std::array<vec128, I>& arr0, const std::array<vec128, I>& arr1, std::array<vec128, I>& arr2)\n {\n-    vec128& x = std::get<ITER>(arr0);\n-    const vec128& y = std::get<ITER>(arr1);\n-    vec128& z = std::get<ITER>(arr2);\n-\n-    x += y;\n-    z ^= x;\n-    vec_rotl<BITS>(z);\n+    vec_add_xor_rot<BITS>(std::get<ITER>(arr0), std::get<ITER>(arr1), std::get<ITER>(arr2));\n\n     if constexpr(ITER + 1 < I ) arr_add_xor_rot<BITS, I, ITER + 1>(arr0, arr1, arr2);\n }\n\n```"
  },
  {
   "t": "2026-08-29T23:20:47Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_128impl.h",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "`arr_shuf0`, `arr_shuf1`, and `arr_shuf2` differ only in their lane rotation, while `doubleround` carries an unused template parameter.\n\nCould we express each rotation through `arr_shuf<ROT>` and drop the unused parameter?\n\ngeneralize ChaCha20 vector shuffles\n\n```patch\ndiff --git a/src/crypto/chacha20_vec_128impl.h b/src/crypto/chacha20_vec_128impl.h\nindex 9342c78b6d..92b0148d75 100644\n--- a/src/crypto/chacha20_vec_128impl.h\n+++ b/src/crypto/chacha20_vec_128impl.h\n@@ -118,60 +118,43 @@ The second round:\n             QUARTERROUND( x2, x7, x8,x13);\n             QUARTERROUND( x3, x4, x9,x14);\n\n-After the first round, arr_shuf0, arr_shuf1, and arr_shuf2 are used to shuffle\n-the layout to prepare for the second round.\n+After the first round, arr_shuf<1>, arr_shuf<2> and arr_shuf<3> rotate the\n+lanes to prepare for the second round.\n\n-After the second round, they are used (in reverse) to restore the original\n-layout.\n+After the second round, the same rotations are applied in reverse to restore\n+the original layout.\n\n */\n\n-template <size_t I, size_t ITER = 0>\n-ALWAYS_INLINE void arr_shuf0(std::array<vec128, I>& arr)\n-{\n-    vec128& x = std::get<ITER>(arr);\n-    x = vec128{x[1], x[2], x[3], x[0]};\n-\n-    if constexpr(ITER + 1 < I ) arr_shuf0(arr);\n-}\n-\n-template <size_t I, size_t ITER = 0>\n-ALWAYS_INLINE void arr_shuf1(std::array<vec128, I>& arr)\n+/** Rotate the lanes of every array element left by ROT positions */\n+template <size_t ROT, size_t I, size_t ITER = 0>\n+ALWAYS_INLINE void arr_shuf(std::array<vec128, I>& arr)\n {\n     vec128& x = std::get<ITER>(arr);\n-    x = vec128{x[2], x[3], x[0], x[1]};\n+    x = vec128{x[ROT % 4], x[(ROT + 1) % 4], x[(ROT + 2) % 4], x[(ROT + 3) % 4]};\n\n-    if constexpr(ITER + 1 < I ) arr_shuf1(arr);\n-}\n-\n-template <size_t I, size_t ITER = 0>\n-ALWAYS_INLINE void arr_shuf2(std::array<vec128, I>& arr)\n-{\n-    vec128& x = std::get<ITER>(arr);\n-    x = vec128{x[3], x[0], x[1], x[2]};\n-\n-    if constexpr(ITER + 1 < I ) arr_shuf2(arr);\n+    if constexpr (ITER + 1 < I) arr_shuf<ROT, I, ITER + 1>(arr);\n }\n\n /* Main round function. */\n-template <size_t I, size_t ITER = 0>\n-ALWAYS_INLINE void doubleround(std::array<vec128, I>& arr0, std::array<vec128, I>& arr1, std::array<vec128, I>&arr2, std::array<vec128, I>&arr3)\n+template <size_t I>\n+ALWAYS_INLINE void doubleround(std::array<vec128, I>& arr0, std::array<vec128, I>& arr1, std::array<vec128, I>& arr2, std::array<vec128, I>& arr3)\n {\n     for(unsigned i = 0; i < 10; i++) {\n         arr_add_xor_rot<16>(arr0, arr1, arr3);\n         arr_add_xor_rot<12>(arr2, arr3, arr1);\n         arr_add_xor_rot<8>(arr0, arr1, arr3);\n         arr_add_xor_rot<7>(arr2, arr3, arr1);\n-        arr_shuf0(arr1);\n-        arr_shuf1(arr2);\n-        arr_shuf2(arr3);\n+        arr_shuf<1>(arr1);\n+        arr_shuf<2>(arr2);\n+        arr_shuf<3>(arr3);\n         arr_add_xor_rot<16>(arr0, arr1, arr3);\n         arr_add_xor_rot<12>(arr2, arr3, arr1);\n         arr_add_xor_rot<8>(arr0, arr1, arr3);\n         arr_add_xor_rot<7>(arr2, arr3, arr1);\n-        arr_shuf2(arr1);\n-        arr_shuf1(arr2);\n-        arr_shuf0(arr3);\n+        arr_shuf<3>(arr1);\n+        arr_shuf<2>(arr2);\n+        arr_shuf<1>(arr3);\n     }\n }\n```"
  },
  {
   "t": "2026-08-29T23:24:19Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.ipp",
   "commit": "cdfca84e235ead4306aadbf27df561964bcd6c2e",
   "in_reply_to": 2626253726,
   "text": "This still stands: `vec_byteswap` duplicates the host-endian branch around raw `__builtin_bswap32` calls, while `compat/endian.h` already provides the same lane conversion through `htole32_internal`.\n\nreuse endian helper for ChaCha20\n\n```patch\ndiff --git a/src/crypto/chacha20_vec_128impl.h b/src/crypto/chacha20_vec_128impl.h\nindex 92b0148d75..85b4e77395 100644\n--- a/src/crypto/chacha20_vec_128impl.h\n+++ b/src/crypto/chacha20_vec_128impl.h\n@@ -6,11 +6,11 @@\n #define BITCOIN_CRYPTO_CHACHA20_VEC_128IMPL_H\n\n #include <attributes.h>\n+#include <compat/endian.h>\n #include <crypto/chacha20_vec.h>\n\n #include <algorithm>\n #include <array>\n-#include <bit>\n #include <cassert>\n #include <cstdint>\n #include <cstring>\n@@ -22,18 +22,10 @@ static constexpr size_t BLOCKLEN = 64;\n\n using vec128 = uint32_t __attribute__((__vector_size__(16)));\n\n-/** Endian-conversion for big-endian */\n+/** Convert every lane to little-endian byte order (a no-op on little-endian hosts) */\n ALWAYS_INLINE void vec_byteswap(vec128& vec)\n {\n-    if constexpr (std::endian::native == std::endian::big)\n-    {\n-        vec128 ret;\n-        ret[0] = __builtin_bswap32(vec[0]);\n-        ret[1] = __builtin_bswap32(vec[1]);\n-        ret[2] = __builtin_bswap32(vec[2]);\n-        ret[3] = __builtin_bswap32(vec[3]);\n-        vec = ret;\n-    }\n+    vec = vec128{htole32_internal(vec[0]), htole32_internal(vec[1]), htole32_internal(vec[2]), htole32_internal(vec[3])};\n }\n\n /** Left-rotate vector */\n```\n\nBut we could go a step futher and just inline it and use `ReadLE32` and `WriteLE32` which already express the required unaligned endian conversion.\n\nreuse ChaCha20 endian I/O\n\n```patch\ndiff --git a/src/crypto/chacha20_vec_128impl.h b/src/crypto/chacha20_vec_128impl.h\nindex 92b0148d75..988174d02c 100644\n--- a/src/crypto/chacha20_vec_128impl.h\n+++ b/src/crypto/chacha20_vec_128impl.h\n@@ -6,14 +6,13 @@\n #define BITCOIN_CRYPTO_CHACHA20_VEC_128IMPL_H\n\n #include <attributes.h>\n+#include <crypto/common.h>\n #include <crypto/chacha20_vec.h>\n\n #include <algorithm>\n #include <array>\n-#include <bit>\n #include <cassert>\n #include <cstdint>\n-#include <cstring>\n #include <span>\n\n namespace chacha20_vec128 {\n@@ -22,20 +21,6 @@ static constexpr size_t BLOCKLEN = 64;\n\n using vec128 = uint32_t __attribute__((__vector_size__(16)));\n\n-/** Endian-conversion for big-endian */\n-ALWAYS_INLINE void vec_byteswap(vec128& vec)\n-{\n-    if constexpr (std::endian::native == std::endian::big)\n-    {\n-        vec128 ret;\n-        ret[0] = __builtin_bswap32(vec[0]);\n-        ret[1] = __builtin_bswap32(vec[1]);\n-        ret[2] = __builtin_bswap32(vec[2]);\n-        ret[3] = __builtin_bswap32(vec[3]);\n-        vec = ret;\n-    }\n-}\n-\n /** Left-rotate vector */\n template <size_t BITS>\n ALWAYS_INLINE void vec_rotl(vec128& vec)\n@@ -161,13 +146,11 @@ ALWAYS_INLINE void doubleround(std::array<vec128, I>& arr0, std::array<vec128, I\n /** XOR 16 input bytes with one state row and write them without assuming alignment or vec128's memory layout */\n ALWAYS_INLINE void vec_read_xor_write(std::span<const std::byte, 16> in_bytes, std::span<std::byte, 16> out_bytes, const vec128& vec)\n {\n-    std::array<uint32_t, 4> temparr;\n-    memcpy(temparr.data(), in_bytes.data(), in_bytes.size());\n-    vec128 tempvec = vec;\n-    vec_byteswap(tempvec);\n-    tempvec ^= (vec128){temparr[0], temparr[1], temparr[2], temparr[3]};\n-    temparr = {tempvec[0], tempvec[1], tempvec[2], tempvec[3]};\n-    memcpy(out_bytes.data(), temparr.data(), out_bytes.size());\n+    const vec128 result{vec ^ vec128{ReadLE32(in_bytes.data()), ReadLE32(in_bytes.data() + 4), ReadLE32(in_bytes.data() + 8), ReadLE32(in_bytes.data() + 12)}};\n+    WriteLE32(out_bytes.data(), result[0]);\n+    WriteLE32(out_bytes.data() + 4, result[1]);\n+    WriteLE32(out_bytes.data() + 8, result[2]);\n+    WriteLE32(out_bytes.data() + 12, result[3]);\n }\n\n /** Write each 64-byte state in row order */\n```\n\nAnd while we're here, the implementation indexes `vec128` as four 32-bit lanes, but does not verify that the compiler honored the `vector_size` attribute, let's assert it to be sure.\n\nverify ChaCha20 vector width\n\n```patch\ndiff --git a/src/crypto/chacha20_vec_128impl.h b/src/crypto/chacha20_vec_128impl.h\nindex 32dc166952..9545c4d8da 100644\n--- a/src/crypto/chacha20_vec_128impl.h\n+++ b/src/crypto/chacha20_vec_128impl.h\n@@ -20,6 +20,7 @@ namespace chacha20_vec128 {\n static constexpr size_t BLOCKLEN = 64;\n\n using vec128 = uint32_t __attribute__((__vector_size__(16)));\n+static_assert(sizeof(vec128) == 16);\n\n /** Left-rotate vector */\n template <size_t BITS>\n```"
  },
  {
   "t": "2026-08-29T23:27:49Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.cpp",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "The existing ChaCha20 benchmarks jump from 4 blocks to 16,384 blocks, so they do not isolate the 2-3-block paths or AArch64 exact 12-15-state groups.\n\nInstead of documenting how each platform behaved (or next to, as @ajtowns suggested), could we cover those four boundary sizes?\n\nbenchmark ChaCha20 dispatch groups\n\n```patch\ndiff --git a/src/bench/chacha20.cpp b/src/bench/chacha20.cpp\nindex cc2b57ebbe..c1c5aed142 100644\n--- a/src/bench/chacha20.cpp\n+++ b/src/bench/chacha20.cpp\n@@ -48,11 +48,41 @@ static void CHACHA20_64BYTES(benchmark::Bench& bench)\n     CHACHA20(bench, BUFFER_SIZE_TINY);\n }\n\n+static void CHACHA20_128BYTES(benchmark::Bench& bench)\n+{\n+    CHACHA20(bench, ChaCha20Aligned::BLOCKLEN * 2);\n+}\n+\n+static void CHACHA20_192BYTES(benchmark::Bench& bench)\n+{\n+    CHACHA20(bench, ChaCha20Aligned::BLOCKLEN * 3);\n+}\n+\n static void CHACHA20_256BYTES(benchmark::Bench& bench)\n {\n     CHACHA20(bench, BUFFER_SIZE_SMALL);\n }\n\n+static void CHACHA20_768BYTES(benchmark::Bench& bench)\n+{\n+    CHACHA20(bench, ChaCha20Aligned::BLOCKLEN * 12);\n+}\n+\n+static void CHACHA20_832BYTES(benchmark::Bench& bench)\n+{\n+    CHACHA20(bench, ChaCha20Aligned::BLOCKLEN * 13);\n+}\n+\n+static void CHACHA20_896BYTES(benchmark::Bench& bench)\n+{\n+    CHACHA20(bench, ChaCha20Aligned::BLOCKLEN * 14);\n+}\n+\n+static void CHACHA20_960BYTES(benchmark::Bench& bench)\n+{\n+    CHACHA20(bench, ChaCha20Aligned::BLOCKLEN * 15);\n+}\n+\n static void CHACHA20_1MB(benchmark::Bench& bench)\n {\n     CHACHA20(bench, BUFFER_SIZE_LARGE);\n@@ -74,7 +104,13 @@ static void FSCHACHA20POLY1305_1MB(benchmark::Bench& bench)\n }\n\n BENCHMARK(CHACHA20_64BYTES);\n+BENCHMARK(CHACHA20_128BYTES);\n+BENCHMARK(CHACHA20_192BYTES);\n BENCHMARK(CHACHA20_256BYTES);\n+BENCHMARK(CHACHA20_768BYTES);\n+BENCHMARK(CHACHA20_832BYTES);\n+BENCHMARK(CHACHA20_896BYTES);\n+BENCHMARK(CHACHA20_960BYTES);\n BENCHMARK(CHACHA20_1MB);\n BENCHMARK(FSCHACHA20POLY1305_64BYTES);\n BENCHMARK(FSCHACHA20POLY1305_256BYTES);\n```\n\n--------\n\nSimilarly, the `FSChaCha20Poly1305` benchmarks jump from 256 bytes to 1 MiB, so they miss the partial-to-exact fifth ChaCha block transition used in the BIP324 comparison.\n\nCould we benchmark 318, 319, and 320-byte inputs to keep that P2P-sized transition visible?\n\nbenchmark BIP324 ChaCha sizes\n\n```patch\ndiff --git a/src/bench/chacha20.cpp b/src/bench/chacha20.cpp\nindex c1c5aed142..264d3fbd94 100644\n--- a/src/bench/chacha20.cpp\n+++ b/src/bench/chacha20.cpp\n@@ -98,6 +98,21 @@ static void FSCHACHA20POLY1305_256BYTES(benchmark::Bench& bench)\n     FSCHACHA20POLY1305(bench, BUFFER_SIZE_SMALL);\n }\n\n+static void FSCHACHA20POLY1305_BIP324_318BYTES(benchmark::Bench& bench)\n+{\n+    FSCHACHA20POLY1305(bench, 318);\n+}\n+\n+static void FSCHACHA20POLY1305_BIP324_319BYTES(benchmark::Bench& bench)\n+{\n+    FSCHACHA20POLY1305(bench, 319);\n+}\n+\n+static void FSCHACHA20POLY1305_BIP324_320BYTES(benchmark::Bench& bench)\n+{\n+    FSCHACHA20POLY1305(bench, 320);\n+}\n+\n static void FSCHACHA20POLY1305_1MB(benchmark::Bench& bench)\n {\n     FSCHACHA20POLY1305(bench, BUFFER_SIZE_LARGE);\n@@ -114,4 +129,7 @@ BENCHMARK(CHACHA20_960BYTES);\n BENCHMARK(CHACHA20_1MB);\n BENCHMARK(FSCHACHA20POLY1305_64BYTES);\n BENCHMARK(FSCHACHA20POLY1305_256BYTES);\n+BENCHMARK(FSCHACHA20POLY1305_BIP324_318BYTES);\n+BENCHMARK(FSCHACHA20POLY1305_BIP324_319BYTES);\n+BENCHMARK(FSCHACHA20POLY1305_BIP324_320BYTES);\n BENCHMARK(FSCHACHA20POLY1305_1MB);\n```"
  },
  {
   "t": "2026-08-29T23:32:36Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20.cpp",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "The AArch64 backend handles 4 or more blocks, yet 1-3-block `Crypt` calls still construct its vector state before returning to scalar code.\n\nCould we expose the backend's minimum block count and skip shorter calls consistently (diff is against my other changes that I applied locally, may not apply cleanly)?\n\nskip short ChaCha20 vector calls\n\n```patch\ndiff --git a/src/crypto/chacha20.cpp b/src/crypto/chacha20.cpp\nindex 891a157d57..9a833702b0 100644\n--- a/src/crypto/chacha20.cpp\n+++ b/src/crypto/chacha20.cpp\n@@ -290,8 +290,10 @@ inline void ChaCha20Aligned::Crypt(std::span<const std::byte> in_bytes, std::spa\n     // The vectorized implementation cannot increment the first nonce word\n     const size_t blocks{out_bytes.size() / BLOCKLEN};\n     assert(blocks < std::numeric_limits<uint32_t>::max() - input[8]);\n-    chacha20_vec::chacha20_crypt_vectorized(in_bytes, out_bytes, input);\n-    input[8] += blocks - out_bytes.size() / BLOCKLEN;\n+    if (blocks >= chacha20_vec::MIN_BLOCKS) {\n+        chacha20_vec::chacha20_crypt_vectorized(in_bytes, out_bytes, input);\n+        input[8] += blocks - out_bytes.size() / BLOCKLEN;\n+    }\n #endif\n     chacha20_crypt(in_bytes, out_bytes, input);\n }\ndiff --git a/src/crypto/chacha20_vec.cpp b/src/crypto/chacha20_vec.cpp\nindex 82ce9bc633..0b2a2e0114 100644\n--- a/src/crypto/chacha20_vec.cpp\n+++ b/src/crypto/chacha20_vec.cpp\n@@ -43,7 +43,7 @@ namespace {\n     // 12 is faster than 8 + 4\n     while (in_bytes.size() >= BLOCKLEN) {\n         const size_t blocks = out_bytes.size() / BLOCKLEN;\n-        if (blocks < 4) break;\n+        if (blocks < MIN_BLOCKS) break;\n         switch (blocks) {\n         case 4: crypter.CryptStates<4>(in_bytes, out_bytes); break;\n         case 5: crypter.CryptStates<5>(in_bytes, out_bytes); break;\ndiff --git a/src/crypto/chacha20_vec.h b/src/crypto/chacha20_vec.h\nindex 9b58dd6d1c..0c3a335026 100644\n--- a/src/crypto/chacha20_vec.h\n+++ b/src/crypto/chacha20_vec.h\n@@ -31,6 +31,7 @@ constexpr VectorTarget TARGET{VectorTarget::X86_64};\n #elif defined(__aarch64__)\n constexpr VectorTarget TARGET{VectorTarget::AARCH64};\n #endif\n+constexpr uint16_t MIN_BLOCKS{TARGET == VectorTarget::AARCH64 ? 4 : 1}; // Shorter AArch64 inputs are faster in scalar code\n\n void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept;\n } // namespace chacha20_vec\n```"
  },
  {
   "t": "2026-08-29T23:43:31Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20.cpp",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "Slightly unrelated, but my IDE flagged these: `ChaCha20::Keystream` expresses fixed-count buffer copies with matching end iterators, while both buffering paths leave derived counts mutable and `Crypt` checks a size only for emptiness.\n\nsimplify ChaCha20 buffered paths\n\n```patch\ndiff --git a/src/crypto/chacha20.cpp b/src/crypto/chacha20.cpp\nindex 9a833702b0..140f2baab9 100644\n--- a/src/crypto/chacha20.cpp\n+++ b/src/crypto/chacha20.cpp\n@@ -303,7 +303,7 @@ void ChaCha20::Keystream(std::span<std::byte> out) noexcept\n     if (out.empty()) return;\n     if (m_bufleft) {\n         unsigned reuse = std::min<size_t>(m_bufleft, out.size());\n-        std::copy(m_buffer.end() - m_bufleft, m_buffer.end() - m_bufleft + reuse, out.begin());\n+        std::copy_n(m_buffer.end() - m_bufleft, reuse, out.begin());\n         m_bufleft -= reuse;\n         out = out.subspan(reuse);\n     }\n@@ -314,7 +314,7 @@ void ChaCha20::Keystream(std::span<std::byte> out) noexcept\n     }\n     if (!out.empty()) {\n         m_aligned.Keystream(m_buffer);\n-        std::copy(m_buffer.begin(), m_buffer.begin() + out.size(), out.begin());\n+        std::copy_n(m_buffer.begin(), out.size(), out.begin());\n         m_bufleft = m_aligned.BLOCKLEN - out.size();\n     }\n }\n@@ -323,7 +323,7 @@ void ChaCha20::Crypt(std::span<const std::byte> input, std::span<std::byte> outp\n {\n     assert(input.size() == output.size());\n\n-    if (!input.size()) return;\n+    if (input.empty()) return;\n     if (m_bufleft) {\n         unsigned reuse = std::min<size_t>(m_bufleft, input.size());\n         for (unsigned i = 0; i < reuse; i++) {\n```"
  },
  {
   "t": "2026-08-30T00:23:13Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.cpp",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "My concern with all these branches is that I'm not sure all of them are properly tested.\n\nThe differential fuzzer currently chooses an arbitrary byte count and stops at 64 blocks.\n\nCould we derive the input size from named `blocks` in `[0, 100]` plus `tail_bytes` in `[0, BLOCKLEN - 1]`? This preserves empty inputs, makes every vector group directly selectable, and keeps the\nimplementation as the independent oracle.\n\nfuzz ChaCha20 vector groups\n\n```patch\ndiff --git a/src/test/fuzz/crypto_diff_fuzz_chacha20.cpp b/src/test/fuzz/crypto_diff_fuzz_chacha20.cpp\nindex 5e2f84d621..34cf8447b4 100644\n--- a/src/test/fuzz/crypto_diff_fuzz_chacha20.cpp\n+++ b/src/test/fuzz/crypto_diff_fuzz_chacha20.cpp\n@@ -322,18 +322,21 @@ FUZZ_TARGET(crypto_diff_fuzz_chacha20)\n                 assert(counter == ctx.input[12]);\n             },\n             [&] {\n-                uint32_t integralInRange = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, 4096);\n-                std::vector<uint8_t> output(integralInRange);\n+                constexpr uint16_t MAX_BLOCKS{100}; // Leaves room for future 256- and 512-bit vector dispatches\n+                const uint16_t blocks{fuzzed_data_provider.ConsumeIntegralInRange<uint16_t>(0, MAX_BLOCKS)};\n+                const uint8_t tail_bytes{fuzzed_data_provider.ConsumeIntegralInRange<uint8_t>(0, ChaCha20Aligned::BLOCKLEN - 1)};\n+                const uint32_t size{uint32_t{blocks} * ChaCha20Aligned::BLOCKLEN + tail_bytes};\n+                std::vector<uint8_t> output(size);\n                 const std::vector<uint8_t> input = ConsumeFixedLengthByteVector(fuzzed_data_provider, output.size());\n                 chacha20.Crypt(MakeByteSpan(input), MakeWritableByteSpan(output));\n-                std::vector<uint8_t> djb_output(integralInRange);\n+                std::vector<uint8_t> djb_output(size);\n                 ECRYPT_encrypt_bytes(&ctx, input.data(), djb_output.data(), input.size());\n                 assert(output == djb_output);\n                 // DJB's version seeks forward to a multiple of 64 bytes after every operation. Correct for that.\n                 uint32_t old_counter = counter;\n-                counter += (integralInRange + 63) >> 6;\n+                counter += (size + ChaCha20Aligned::BLOCKLEN - 1) / ChaCha20Aligned::BLOCKLEN;\n                 if (counter < old_counter) ++nonce.first;\n-                if (integralInRange & 63) {\n+                if (size % ChaCha20Aligned::BLOCKLEN) {\n                     chacha20.Seek(nonce, counter);\n                 }\n                 assert(counter == ctx.input[12]);\n```"
  },
  {
   "t": "2026-08-30T00:25:40Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.cpp",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "Is 15 also faster than e.g. 8+7? On my M4 Max, processing 14 and 15 blocks as 8+6 and 8+7 is faster than using exact 14- and 15-state groups."
  },
  {
   "t": "2026-08-30T00:27:08Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha_vec_impl.h",
   "commit": "5155730de55bdbada706ffaa39a827de7370efb8",
   "in_reply_to": 2625335880,
   "text": "That's fine in a draft I guess, but in the final version I'd also appreciate a reformat - my OCD keeps flaring up for these :p"
  },
  {
   "t": "2026-08-30T01:27:04Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_128impl.h",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "These shuffles temporarily view the same 128 bits as 16- or 8-bit lanes, then convert the result back to four 32-bit lanes.\n\nCould we use `std::bit_cast` for both same-size conversions instead of `reinterpret_cast` and the C-style cast?\n\nbit-cast ChaCha20 vector lanes\n\n```patch\ndiff --git a/src/crypto/chacha20_vec_128impl.h b/src/crypto/chacha20_vec_128impl.h\nindex f8fcbfb2ec..f77c50f5c7 100644\n--- a/src/crypto/chacha20_vec_128impl.h\n+++ b/src/crypto/chacha20_vec_128impl.h\n@@ -10,6 +10,7 @@\n\n #include <algorithm>\n #include <array>\n+#include <bit>\n #include <cassert>\n #include <cstdint>\n #include <span>\n@@ -34,7 +35,8 @@ ALWAYS_INLINE\n void vec_rotl<16>(vec128& vec)\n {\n     using vec128_u16 = uint16_t __attribute__((__vector_size__(16)));\n-    vec = (vec128)__builtin_shufflevector(reinterpret_cast<vec128_u16>(vec), vec128_u16{}, 1, 0, 3, 2, 5, 4, 7, 6);\n+    const auto halfwords{std::bit_cast<vec128_u16>(vec)};\n+    vec = std::bit_cast<vec128>(__builtin_shufflevector(halfwords, vec128_u16{}, 1, 0, 3, 2, 5, 4, 7, 6));\n }\n #endif\n\n@@ -44,7 +46,8 @@ ALWAYS_INLINE\n void vec_rotl<8>(vec128& vec)\n {\n     using vec128_u8 = uint8_t __attribute__((__vector_size__(16)));\n-    vec = (vec128)__builtin_shufflevector(reinterpret_cast<vec128_u8>(vec), vec128_u8{}, 3,0,1,2,7,4,5,6,11,8,9,10,15,12,13,14);\n+    const auto bytes{std::bit_cast<vec128_u8>(vec)};\n+    vec = std::bit_cast<vec128>(__builtin_shufflevector(bytes, vec128_u8{}, 3, 0, 1, 2, 7, 4, 5, 6, 11, 8, 9, 10, 15, 12, 13, 14));\n }\n #endif\n\n```\n\nNit: formatting"
  },
  {
   "t": "2026-08-30T01:36:06Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec.cpp",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "Corecheck measures the one-block vector path slower, while `CryptStates<3>` improved the 192-byte median from 0.98 to 0.86 ns/byte on this Apple M4 Max.\n\nCould we expose target minimums of two blocks on x86-64 and three on AArch64, and leave shorter calls scalar?"
  },
  {
   "t": "2026-08-30T03:28:57Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/CMakeLists.txt",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "The dependency lint treats matching `.h` and `.cpp` basenames as one module, which creates `chacha20_vec -> chacha20_vec_128impl -> chacha20_vec` if the implementation header includes the shared interface.\n\nCould we rename this TU to `chacha20_vec_dispatch.cpp`, include `chacha20_vec.h` from the implementation header, and keep one definition of `BLOCKLEN` and `STATE_WORDS`?"
  },
  {
   "t": "2026-08-30T03:31:42Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_128impl.h",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "This function always calls the non-`constexpr` `multi_block_crypt`, so it cannot be evaluated at compile time, we can probably drop the `constexpr`."
  },
  {
   "t": "2026-08-30T03:32:10Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/crypto/chacha20_vec_128impl.h",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "in_reply_to": null,
   "text": "nit:  Could we make this `explicit`?"
  },
  {
   "t": "2026-08-30T03:53:40Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "CHANGES_REQUESTED",
   "commit": "8364a78e147708d62cd7dbb6f6a0ff98022c9e9a",
   "text": "I went through the current version and left inline comments.\n\nThe specialization itself is not particularly complicated, but it is sensitive to compiler and architecture changes. I expect the dispatch and grouping choices to need retuning every few years, so I would prefer them to remain centralized and configurable instead of being spread across manual unrolls or duplicated control flow.\n\nBefore this leaves draft, it would help to restructure the PR into smaller, independently reviewable commits that separate preparatory changes, the vector backend, its adoption, benchmarks, and correctness coverage. The title, description, commit messages, and several comments still describe the earlier 256-bit attempt. The implementation can also reuse existing infrastructure, particularly `ALWAYS_INLINE` from `attributes.h` and the endian helpers.\n\n[PR #286](https://github.com/l0rinc/bitcoin/pull/286) shows the final structure and coverage I have in mind. It includes most comments I added here.\n\n----\n\n[Corecheck](https://corecheck.dev/bitcoin/bitcoin/pulls/34083) currently has no coverage data for the new code and measures the 64-byte case about 7% slower. The one-block x86-64 path can stay scalar while larger groups keep the vector speedup. The `explicit` constructor and redundant `constexpr` warnings are cheap to address. The macro warning does not apply because `ENABLE_CHACHA20_VEC` controls preprocessing, and `std::bit_cast` makes the same-size vector lane conversions consistent.\n\nTo be more confident in the correctness, I would like to see the new path covered by:\n* tests around every dispatch block count, including one byte below and above each full-block size and in-place operation (see comment).\n* focused benchmarks that isolate every target-specific grouping decision so they can be compared across platforms (see review comment).\n* an extension of the existing `crypto_diff_fuzz_chacha20` target that can select each optimized group and compare it with the existing fuzzer oracle.\n\nI also benchmarked IBD on an Umbrel using master with V1 transport, master with V2 transport, and this PR with V2 transport. The result was surprising: master V1 and V2 were basically the same, while this PR was considerably slower there. I will remeasure future versions once the PR is out of draft, but this is another reason to keep the tuning choices easy to isolate, benchmark, and change."
  }
 ],
 "labels_log": [
  {
   "t": "2026-08-19T00:33:56Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-25T17:50:10Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-08-14T15:24:07Z",
   "kind": "convert_to_draft",
   "who": "fanquake"
  }
 ],
 "text_chars": 105307,
 "text_tokens_estimate": 26326,
 "changed_paths": [],
 "files": [],
 "test_lines": null,
 "git": null,
 "input_hash": "61e617a89d9e0f40",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}