From 7aa557a37b73df264afffdc7c00fba47a339aee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Tue, 15 Jul 2025 14:54:52 -0700 Subject: [PATCH 01/12] random: add fixed-size `std::array` generation Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com> --- src/random.h | 9 +++++++++ src/test/random_tests.cpp | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/random.h b/src/random.h index c702309d0c3..330c10a36b8 100644 --- a/src/random.h +++ b/src/random.h @@ -301,6 +301,15 @@ public: return ret; } + /** Generate fixed-size random bytes. */ + template + std::array randbytes() noexcept + { + std::array ret; + Impl().fillrand(MakeWritableByteSpan(ret)); + return ret; + } + /** Generate a random 32-bit integer. */ uint32_t rand32() noexcept { return Impl().template randbits<32>(); } diff --git a/src/test/random_tests.cpp b/src/test/random_tests.cpp index 3d8b543e64f..538d41125af 100644 --- a/src/test/random_tests.cpp +++ b/src/test/random_tests.cpp @@ -58,7 +58,7 @@ BOOST_AUTO_TEST_CASE(fastrandom_tests_deterministic) BOOST_CHECK_EQUAL(ctx1.rand32(), ctx2.rand32()); BOOST_CHECK_EQUAL(ctx1.rand64(), ctx2.rand64()); BOOST_CHECK_EQUAL(ctx1.randbits(3), ctx2.randbits(3)); - BOOST_CHECK(ctx1.randbytes(17) == ctx2.randbytes(17)); + BOOST_CHECK(std::ranges::equal(ctx1.randbytes(17), ctx2.randbytes<17>())); // check vector/array behavior symmetry BOOST_CHECK(ctx1.rand256() == ctx2.rand256()); BOOST_CHECK_EQUAL(ctx1.randbits(7), ctx2.randbits(7)); BOOST_CHECK(ctx1.randbytes(128) == ctx2.randbytes(128)); From 54ab0bd64c3657f2f9be83ba587a0811e0dd8ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Tue, 15 Jul 2025 14:54:58 -0700 Subject: [PATCH 02/12] refactor: commit to 8 byte obfuscation keys Since 31 byte xor-keys are not used in the codebase, using the common size (8 bytes) makes the benchmarks more realistic. Co-authored-by: maflcko <6399679+maflcko@users.noreply.github.com> --- src/bench/xor.cpp | 3 ++- src/dbwrapper.cpp | 7 +++---- src/dbwrapper.h | 3 --- src/node/blockstorage.cpp | 3 ++- src/node/mempool_persist.cpp | 3 ++- src/test/fuzz/autofile.cpp | 6 ++++-- src/test/fuzz/buffered_file.cpp | 6 ++++-- src/test/streams_tests.cpp | 5 +++-- src/util/obfuscation.h | 16 ++++++++++++++++ 9 files changed, 36 insertions(+), 16 deletions(-) create mode 100644 src/util/obfuscation.h diff --git a/src/bench/xor.cpp b/src/bench/xor.cpp index fc9dc5d1721..f3d6145c2b2 100644 --- a/src/bench/xor.cpp +++ b/src/bench/xor.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -14,7 +15,7 @@ static void Xor(benchmark::Bench& bench) { FastRandomContext frc{/*fDeterministic=*/true}; auto data{frc.randbytes(1024)}; - auto key{frc.randbytes(31)}; + auto key{frc.randbytes(Obfuscation::KEY_SIZE)}; bench.batch(data.size()).unit("byte").run([&] { util::Xor(data, key); diff --git a/src/dbwrapper.cpp b/src/dbwrapper.cpp index 1c35e11863b..19b1adafca5 100644 --- a/src/dbwrapper.cpp +++ b/src/dbwrapper.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -249,7 +250,7 @@ CDBWrapper::CDBWrapper(const DBParams& params) } // The base-case obfuscation key, which is a noop. - obfuscate_key = std::vector(OBFUSCATE_KEY_NUM_BYTES, '\000'); + obfuscate_key = std::vector(Obfuscation::KEY_SIZE, '\000'); bool key_exists = Read(OBFUSCATE_KEY_KEY, obfuscate_key); @@ -316,15 +317,13 @@ size_t CDBWrapper::DynamicMemoryUsage() const // past the null-terminator. const std::string CDBWrapper::OBFUSCATE_KEY_KEY("\000obfuscate_key", 14); -const unsigned int CDBWrapper::OBFUSCATE_KEY_NUM_BYTES = 8; - /** * Returns a string (consisting of 8 random bytes) suitable for use as an * obfuscating XOR key. */ std::vector CDBWrapper::CreateObfuscateKey() const { - std::vector ret(OBFUSCATE_KEY_NUM_BYTES); + std::vector ret(Obfuscation::KEY_SIZE); GetRandBytes(ret); return ret; } diff --git a/src/dbwrapper.h b/src/dbwrapper.h index 789b5be8fc7..64d428ce5de 100644 --- a/src/dbwrapper.h +++ b/src/dbwrapper.h @@ -193,9 +193,6 @@ private: //! the key under which the obfuscation key is stored static const std::string OBFUSCATE_KEY_KEY; - //! the length of the obfuscate key in number of bytes - static const unsigned int OBFUSCATE_KEY_NUM_BYTES; - std::vector CreateObfuscateKey() const; //! path to filesystem storage diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index ba205a9c693..e224c5985d8 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -1123,7 +1124,7 @@ static auto InitBlocksdirXorKey(const BlockManager::Options& opts) { // Bytes are serialized without length indicator, so this is also the exact // size of the XOR-key file. - std::array xor_key{}; + std::array xor_key{}; // Consider this to be the first run if the blocksdir contains only hidden // files (those which start with a .). Checking for a fully-empty dir would diff --git a/src/node/mempool_persist.cpp b/src/node/mempool_persist.cpp index ff47172c274..eeb690b0877 100644 --- a/src/node/mempool_persist.cpp +++ b/src/node/mempool_persist.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -179,7 +180,7 @@ bool DumpMempool(const CTxMemPool& pool, const fs::path& dump_path, FopenFn mock const uint64_t version{pool.m_opts.persist_v1_dat ? MEMPOOL_DUMP_VERSION_NO_XOR_KEY : MEMPOOL_DUMP_VERSION}; file << version; - std::vector xor_key(8); + std::vector xor_key(Obfuscation::KEY_SIZE); if (!pool.m_opts.persist_v1_dat) { FastRandomContext{}.fillrand(xor_key); file << xor_key; diff --git a/src/test/fuzz/autofile.cpp b/src/test/fuzz/autofile.cpp index 8d17624da66..2cebba227f6 100644 --- a/src/test/fuzz/autofile.cpp +++ b/src/test/fuzz/autofile.cpp @@ -4,9 +4,10 @@ #include #include -#include #include +#include #include +#include #include #include @@ -18,9 +19,10 @@ FUZZ_TARGET(autofile) { FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; FuzzedFileProvider fuzzed_file_provider{fuzzed_data_provider}; + const auto key_bytes{ConsumeFixedLengthByteVector(fuzzed_data_provider, Obfuscation::KEY_SIZE)}; AutoFile auto_file{ fuzzed_file_provider.open(), - ConsumeRandomLengthByteVector(fuzzed_data_provider), + key_bytes, }; LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 100) { diff --git a/src/test/fuzz/buffered_file.cpp b/src/test/fuzz/buffered_file.cpp index a6a042a25cb..c61910e55a7 100644 --- a/src/test/fuzz/buffered_file.cpp +++ b/src/test/fuzz/buffered_file.cpp @@ -4,9 +4,10 @@ #include #include -#include #include +#include #include +#include #include #include @@ -20,9 +21,10 @@ FUZZ_TARGET(buffered_file) FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; FuzzedFileProvider fuzzed_file_provider{fuzzed_data_provider}; std::optional opt_buffered_file; + const auto key_bytes{ConsumeFixedLengthByteVector(fuzzed_data_provider, Obfuscation::KEY_SIZE)}; AutoFile fuzzed_file{ fuzzed_file_provider.open(), - ConsumeRandomLengthByteVector(fuzzed_data_provider), + key_bytes, }; try { auto n_buf_size = fuzzed_data_provider.ConsumeIntegralInRange(0, 4096); diff --git a/src/test/streams_tests.cpp b/src/test/streams_tests.cpp index 9e8281d26a7..fbba654285a 100644 --- a/src/test/streams_tests.cpp +++ b/src/test/streams_tests.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -563,7 +564,7 @@ BOOST_AUTO_TEST_CASE(buffered_reader_matches_autofile_random_content) const FlatFilePos pos{0, 0}; const FlatFileSeq test_file{m_args.GetDataDirBase(), "buffered_file_test_random", node::BLOCKFILE_CHUNK_SIZE}; - const std::vector obfuscation{m_rng.randbytes(8)}; + const std::vector obfuscation{m_rng.randbytes(Obfuscation::KEY_SIZE)}; // Write out the file with random content { @@ -618,7 +619,7 @@ BOOST_AUTO_TEST_CASE(buffered_writer_matches_autofile_random_content) const FlatFileSeq test_buffered{m_args.GetDataDirBase(), "buffered_write_test", node::BLOCKFILE_CHUNK_SIZE}; const FlatFileSeq test_direct{m_args.GetDataDirBase(), "direct_write_test", node::BLOCKFILE_CHUNK_SIZE}; - const std::vector obfuscation{m_rng.randbytes(8)}; + const std::vector obfuscation{m_rng.randbytes(Obfuscation::KEY_SIZE)}; { DataBuffer test_data{m_rng.randbytes(file_size)}; diff --git a/src/util/obfuscation.h b/src/util/obfuscation.h new file mode 100644 index 00000000000..628dacfc9d5 --- /dev/null +++ b/src/util/obfuscation.h @@ -0,0 +1,16 @@ +// Copyright (c) 2025-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_UTIL_OBFUSCATION_H +#define BITCOIN_UTIL_OBFUSCATION_H + +#include + +class Obfuscation +{ +public: + static constexpr size_t KEY_SIZE{sizeof(uint64_t)}; +}; + +#endif // BITCOIN_UTIL_OBFUSCATION_H From a5141cd39ecbd3a7bbae5bf2f755cc5aa7c41da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Wed, 16 Jul 2025 14:09:56 -0700 Subject: [PATCH 03/12] test: make sure dbwrapper obfuscation key is never obfuscated --- src/test/dbwrapper_tests.cpp | 60 +++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/src/test/dbwrapper_tests.cpp b/src/test/dbwrapper_tests.cpp index 3a86036327c..bd50db17c13 100644 --- a/src/test/dbwrapper_tests.cpp +++ b/src/test/dbwrapper_tests.cpp @@ -30,18 +30,50 @@ BOOST_AUTO_TEST_CASE(dbwrapper) { // Perform tests both obfuscated and non-obfuscated. for (const bool obfuscate : {false, true}) { - fs::path ph = m_args.GetDataDirBase() / (obfuscate ? "dbwrapper_obfuscate_true" : "dbwrapper_obfuscate_false"); - CDBWrapper dbw({.path = ph, .cache_bytes = 1 << 20, .memory_only = true, .wipe_data = false, .obfuscate = obfuscate}); - uint8_t key{'k'}; - uint256 in = m_rng.rand256(); - uint256 res; + constexpr size_t CACHE_SIZE{1_MiB}; + const fs::path path{m_args.GetDataDirBase() / "dbwrapper"}; - // Ensure that we're doing real obfuscation when obfuscate=true - BOOST_CHECK(obfuscate != is_null_key(dbwrapper_private::GetObfuscateKey(dbw))); + std::vector obfuscation_key{}; + std::vector> key_values{}; - BOOST_CHECK(dbw.Write(key, in)); - BOOST_CHECK(dbw.Read(key, res)); - BOOST_CHECK_EQUAL(res.ToString(), in.ToString()); + // Write values + { + CDBWrapper dbw{{.path = path, .cache_bytes = CACHE_SIZE, .wipe_data = true, .obfuscate = obfuscate}}; + BOOST_CHECK_EQUAL(obfuscate, !dbw.IsEmpty()); + + // Ensure that we're doing real obfuscation when obfuscate=true + obfuscation_key = dbwrapper_private::GetObfuscateKey(dbw); + BOOST_CHECK_EQUAL(obfuscate, !is_null_key(obfuscation_key)); + + for (uint8_t k{0}; k < 10; ++k) { + uint8_t key{k}; + uint256 value{m_rng.rand256()}; + BOOST_CHECK(dbw.Write(key, value)); + key_values.emplace_back(key, value); + } + } + + // Verify that the obfuscation key is never obfuscated + { + CDBWrapper dbw{{.path = path, .cache_bytes = CACHE_SIZE, .obfuscate = false}}; + BOOST_CHECK(obfuscation_key == dbwrapper_private::GetObfuscateKey(dbw)); + } + + // Read back the values + { + CDBWrapper dbw{{.path = path, .cache_bytes = CACHE_SIZE, .obfuscate = obfuscate}}; + + // Ensure obfuscation is read back correctly + BOOST_CHECK(obfuscation_key == dbwrapper_private::GetObfuscateKey(dbw)); + BOOST_CHECK_EQUAL(obfuscate, !is_null_key(obfuscation_key)); + + // Verify all written values + for (const auto& [key, expected_value] : key_values) { + uint256 read_value{}; + BOOST_CHECK(dbw.Read(key, read_value)); + BOOST_CHECK_EQUAL(read_value, expected_value); + } + } } } @@ -57,7 +89,7 @@ BOOST_AUTO_TEST_CASE(dbwrapper_basic_data) bool res_bool; // Ensure that we're doing real obfuscation when obfuscate=true - BOOST_CHECK(obfuscate != is_null_key(dbwrapper_private::GetObfuscateKey(dbw))); + BOOST_CHECK_EQUAL(obfuscate, !is_null_key(dbwrapper_private::GetObfuscateKey(dbw))); //Simulate block raw data - "b + block hash" std::string key_block = "b" + m_rng.rand256().ToString(); @@ -116,13 +148,13 @@ BOOST_AUTO_TEST_CASE(dbwrapper_basic_data) std::string file_option_tag = "F"; uint8_t filename_length = m_rng.randbits(8); std::string filename = "randomfilename"; - std::string key_file_option = strprintf("%s%01x%s", file_option_tag,filename_length,filename); + std::string key_file_option = strprintf("%s%01x%s", file_option_tag, filename_length, filename); bool in_file_bool = m_rng.randbool(); BOOST_CHECK(dbw.Write(key_file_option, in_file_bool)); BOOST_CHECK(dbw.Read(key_file_option, res_bool)); BOOST_CHECK_EQUAL(res_bool, in_file_bool); - } + } } // Test batch operations @@ -231,7 +263,7 @@ BOOST_AUTO_TEST_CASE(existing_data_no_obfuscate) BOOST_CHECK(odbw.Read(key, res2)); BOOST_CHECK_EQUAL(res2.ToString(), in.ToString()); - BOOST_CHECK(!odbw.IsEmpty()); // There should be existing data + BOOST_CHECK(!odbw.IsEmpty()); BOOST_CHECK(is_null_key(dbwrapper_private::GetObfuscateKey(odbw))); // The key should be an empty string uint256 in2 = m_rng.rand256(); From 618a30e326e9bcfd72e0e2645ce49f8b2a88714d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Wed, 16 Jul 2025 14:09:59 -0700 Subject: [PATCH 04/12] test: compare util::Xor with randomized inputs against simple impl The two tests are doing different things - `xor_roundtrip_random_chunks` does black-box style property-based testing to validate that certain invariants hold - that deobfuscating an obfuscation results in the original message (higher level, it doesn't have to know about the implementation details). The `xor_bytes_reference` test makes sure the optimized xor implementation behaves in every imaginable scenario exactly as the simplest possible obfuscation - with random chunks, random alignment, random data, random key. Since we're touching the file, other related small refactors were also applied: * `nullpt` typo fixed; * manual byte-by-byte xor key creations were replaced with `_hex` factories; * since we're only using 64 bit keys in production, smaller keys were changed to reflect real-world usage; Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com> --- src/test/streams_tests.cpp | 99 ++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 25 deletions(-) diff --git a/src/test/streams_tests.cpp b/src/test/streams_tests.cpp index fbba654285a..9c1e1032a0f 100644 --- a/src/test/streams_tests.cpp +++ b/src/test/streams_tests.cpp @@ -14,22 +14,79 @@ #include using namespace std::string_literals; +using namespace util::hex_literals; BOOST_FIXTURE_TEST_SUITE(streams_tests, BasicTestingSetup) +// Test that obfuscation can be properly reverted even with random chunk sizes. +BOOST_AUTO_TEST_CASE(xor_roundtrip_random_chunks) +{ + auto apply_random_xor_chunks{[&](std::span target, std::span obfuscation) { + for (size_t offset{0}; offset < target.size();) { + const size_t chunk_size{1 + m_rng.randrange(target.size() - offset)}; + util::Xor(target.subspan(offset, chunk_size), obfuscation, offset); + offset += chunk_size; + } + }}; + + for (size_t test{0}; test < 100; ++test) { + const size_t write_size{1 + m_rng.randrange(100U)}; + const std::vector original{m_rng.randbytes(write_size)}; + std::vector roundtrip{original}; + + const auto key_bytes{m_rng.randbool() ? m_rng.randbytes() : std::array{}}; + apply_random_xor_chunks(roundtrip, key_bytes); + + const bool key_all_zeros{std::ranges::all_of( + std::span{key_bytes}.first(std::min(write_size, Obfuscation::KEY_SIZE)), [](auto b) { return b == std::byte{0}; })}; + BOOST_CHECK(key_all_zeros ? original == roundtrip : original != roundtrip); + + apply_random_xor_chunks(roundtrip, key_bytes); + BOOST_CHECK(original == roundtrip); + } +} + +// Compares optimized obfuscation against a trivial, byte-by-byte reference implementation +// with random offsets to ensure proper handling of key wrapping. +BOOST_AUTO_TEST_CASE(xor_bytes_reference) +{ + auto expected_xor{[](std::span target, std::span obfuscation, size_t key_offset) { + for (auto& b : target) { + b ^= obfuscation[key_offset++ % obfuscation.size()]; + } + }}; + + for (size_t test{0}; test < 100; ++test) { + const size_t write_size{1 + m_rng.randrange(100U)}; + const size_t key_offset{m_rng.randrange(3 * Obfuscation::KEY_SIZE)}; // Make sure the key can wrap around + const size_t write_offset{std::min(write_size, m_rng.randrange(Obfuscation::KEY_SIZE * 2))}; // Write unaligned data + + const auto key_bytes{m_rng.randbool() ? m_rng.randbytes() : std::array{}}; + const std::vector obfuscation{key_bytes.begin(), key_bytes.end()}; + std::vector expected{m_rng.randbytes(write_size)}; + std::vector actual{expected}; + + expected_xor(std::span{expected}.subspan(write_offset), key_bytes, key_offset); + util::Xor(std::span{actual}.subspan(write_offset), key_bytes, key_offset); + + BOOST_CHECK_EQUAL_COLLECTIONS(expected.begin(), expected.end(), actual.begin(), actual.end()); + } +} + BOOST_AUTO_TEST_CASE(xor_file) { fs::path xor_path{m_args.GetDataDirBase() / "test_xor.bin"}; auto raw_file{[&](const auto& mode) { return fsbridge::fopen(xor_path, mode); }}; const std::vector test1{1, 2, 3}; const std::vector test2{4, 5}; - const std::vector xor_pat{std::byte{0xff}, std::byte{0x00}}; + const auto xor_pat{"ff00ff00ff00ff00"_hex_v}; + { // Check errors for missing file AutoFile xor_file{raw_file("rb"), xor_pat}; - BOOST_CHECK_EXCEPTION(xor_file << std::byte{}, std::ios_base::failure, HasReason{"AutoFile::write: file handle is nullpt"}); - BOOST_CHECK_EXCEPTION(xor_file >> std::byte{}, std::ios_base::failure, HasReason{"AutoFile::read: file handle is nullpt"}); - BOOST_CHECK_EXCEPTION(xor_file.ignore(1), std::ios_base::failure, HasReason{"AutoFile::ignore: file handle is nullpt"}); + BOOST_CHECK_EXCEPTION(xor_file << std::byte{}, std::ios_base::failure, HasReason{"AutoFile::write: file handle is nullptr"}); + BOOST_CHECK_EXCEPTION(xor_file >> std::byte{}, std::ios_base::failure, HasReason{"AutoFile::read: file handle is nullptr"}); + BOOST_CHECK_EXCEPTION(xor_file.ignore(1), std::ios_base::failure, HasReason{"AutoFile::ignore: file handle is nullptr"}); } { #ifdef __MINGW64__ @@ -77,7 +134,7 @@ BOOST_AUTO_TEST_CASE(streams_vector_writer) { unsigned char a(1); unsigned char b(2); - unsigned char bytes[] = { 3, 4, 5, 6 }; + unsigned char bytes[] = {3, 4, 5, 6}; std::vector vch; // Each test runs twice. Serializing a second time at the same starting @@ -224,34 +281,26 @@ BOOST_AUTO_TEST_CASE(bitstream_reader_writer) BOOST_AUTO_TEST_CASE(streams_serializedata_xor) { - std::vector in; - // Degenerate case { - DataStream ds{in}; - ds.Xor({0x00, 0x00}); + DataStream ds{}; + ds.Xor("0000000000000000"_hex_v_u8); BOOST_CHECK_EQUAL(""s, ds.str()); } - in.push_back(std::byte{0x0f}); - in.push_back(std::byte{0xf0}); - - // Single character key { - DataStream ds{in}; - ds.Xor({0xff}); + const auto obfuscation{"ffffffffffffffff"_hex_v_u8}; + + DataStream ds{"0ff0"_hex}; + ds.Xor(obfuscation); BOOST_CHECK_EQUAL("\xf0\x0f"s, ds.str()); } - // Multi character key - - in.clear(); - in.push_back(std::byte{0xf0}); - in.push_back(std::byte{0x0f}); - { - DataStream ds{in}; - ds.Xor({0xff, 0x0f}); + const auto obfuscation{"ff0fff0fff0fff0f"_hex_v_u8}; + + DataStream ds{"f00f"_hex}; + ds.Xor(obfuscation); BOOST_CHECK_EQUAL("\x0f\x00"s, ds.str()); } } @@ -564,7 +613,7 @@ BOOST_AUTO_TEST_CASE(buffered_reader_matches_autofile_random_content) const FlatFilePos pos{0, 0}; const FlatFileSeq test_file{m_args.GetDataDirBase(), "buffered_file_test_random", node::BLOCKFILE_CHUNK_SIZE}; - const std::vector obfuscation{m_rng.randbytes(Obfuscation::KEY_SIZE)}; + const auto obfuscation{m_rng.randbytes(Obfuscation::KEY_SIZE)}; // Write out the file with random content { @@ -619,7 +668,7 @@ BOOST_AUTO_TEST_CASE(buffered_writer_matches_autofile_random_content) const FlatFileSeq test_buffered{m_args.GetDataDirBase(), "buffered_write_test", node::BLOCKFILE_CHUNK_SIZE}; const FlatFileSeq test_direct{m_args.GetDataDirBase(), "direct_write_test", node::BLOCKFILE_CHUNK_SIZE}; - const std::vector obfuscation{m_rng.randbytes(Obfuscation::KEY_SIZE)}; + const auto obfuscation{m_rng.randbytes(Obfuscation::KEY_SIZE)}; { DataBuffer test_data{m_rng.randbytes(file_size)}; From 972697976c027b5199150a98e886c199b7ffc335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Fri, 6 Dec 2024 16:18:03 +0100 Subject: [PATCH 05/12] bench: make ObfuscationBench more representative A previous PR already solved the tiny byte-array-xors during serialization, so it makes sense to keep focusing on the performance of bigger continuous chunks. This also renames the file from `xor` to `obfuscation` to enable scripted diff name unification later. > C++ compiler .......................... GNU 14.2.0 | ns/byte | byte/s | err% | ins/byte | cyc/byte | IPC | bra/byte | miss% | total | benchmark |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:---------- | 0.84 | 1,184,138,235.64 | 0.0% | 9.01 | 3.03 | 2.971 | 1.00 | 0.1% | 5.50 | `ObfuscationBench` > C++ compiler .......................... Clang 20.1.7 | ns/byte | byte/s | err% | ins/byte | cyc/byte | IPC | bra/byte | miss% | total | benchmark |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:---------- | 0.89 | 1,124,087,330.23 | 0.1% | 6.52 | 3.20 | 2.041 | 0.50 | 0.2% | 5.50 | `ObfuscationBench` --- src/bench/CMakeLists.txt | 2 +- src/bench/{xor.cpp => obfuscation.cpp} | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) rename src/bench/{xor.cpp => obfuscation.cpp} (59%) diff --git a/src/bench/CMakeLists.txt b/src/bench/CMakeLists.txt index 2137beccb5a..9f161876150 100644 --- a/src/bench/CMakeLists.txt +++ b/src/bench/CMakeLists.txt @@ -35,6 +35,7 @@ add_executable(bench_bitcoin mempool_eviction.cpp mempool_stress.cpp merkle_root.cpp + obfuscation.cpp parse_hex.cpp peer_eviction.cpp poly1305.cpp @@ -51,7 +52,6 @@ add_executable(bench_bitcoin txgraph.cpp util_time.cpp verify_script.cpp - xor.cpp ) include(TargetDataSources) diff --git a/src/bench/xor.cpp b/src/bench/obfuscation.cpp similarity index 59% rename from src/bench/xor.cpp rename to src/bench/obfuscation.cpp index f3d6145c2b2..27a254f8037 100644 --- a/src/bench/xor.cpp +++ b/src/bench/obfuscation.cpp @@ -4,22 +4,23 @@ #include #include -#include #include #include #include #include -static void Xor(benchmark::Bench& bench) +static void ObfuscationBench(benchmark::Bench& bench) { FastRandomContext frc{/*fDeterministic=*/true}; auto data{frc.randbytes(1024)}; - auto key{frc.randbytes(Obfuscation::KEY_SIZE)}; + const auto key{frc.randbytes()}; + size_t offset{0}; bench.batch(data.size()).unit("byte").run([&] { - util::Xor(data, key); + util::Xor(data, key, offset++); // mutated differently each time + ankerl::nanobench::doNotOptimizeAway(data); }); } -BENCHMARK(Xor, benchmark::PriorityLevel::HIGH); +BENCHMARK(ObfuscationBench, benchmark::PriorityLevel::HIGH); From 0b8bec8aa6260c499c2663ab7a1c905da0d312c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Fri, 25 Apr 2025 23:18:48 +0200 Subject: [PATCH 06/12] scripted-diff: unify xor-vs-obfuscation nomenclature Mechanical refactor of the low-level "xor" wording to signal the intent instead of the implementation used. The renames are ordered by heaviest-hitting substitutions first, and were constructed such that after each replacement the code is still compilable. -BEGIN VERIFY SCRIPT- sed -i \ -e 's/\bGetObfuscateKey\b/GetObfuscation/g' \ -e 's/\bxor_key\b/obfuscation/g' \ -e 's/\bxor_pat\b/obfuscation/g' \ -e 's/\bm_xor_key\b/m_obfuscation/g' \ -e 's/\bm_xor\b/m_obfuscation/g' \ -e 's/\bobfuscate_key\b/m_obfuscation/g' \ -e 's/\bOBFUSCATE_KEY_KEY\b/OBFUSCATION_KEY_KEY/g' \ -e 's/\bSetXor(/SetObfuscation(/g' \ -e 's/\bdata_xor\b/obfuscation/g' \ -e 's/\bCreateObfuscateKey\b/CreateObfuscation/g' \ -e 's/\bobfuscate key\b/obfuscation key/g' \ $(git ls-files '*.cpp' '*.h') -END VERIFY SCRIPT- --- src/dbwrapper.cpp | 24 ++++++++++++------------ src/dbwrapper.h | 14 +++++++------- src/node/blockstorage.cpp | 22 +++++++++++----------- src/node/blockstorage.h | 2 +- src/node/mempool_persist.cpp | 14 +++++++------- src/streams.cpp | 14 +++++++------- src/streams.h | 6 +++--- src/test/dbwrapper_tests.cpp | 12 ++++++------ src/test/streams_tests.cpp | 10 +++++----- 9 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/dbwrapper.cpp b/src/dbwrapper.cpp index 19b1adafca5..45f806705b4 100644 --- a/src/dbwrapper.cpp +++ b/src/dbwrapper.cpp @@ -174,7 +174,7 @@ void CDBBatch::Clear() void CDBBatch::WriteImpl(std::span key, DataStream& ssValue) { leveldb::Slice slKey(CharCast(key.data()), key.size()); - ssValue.Xor(dbwrapper_private::GetObfuscateKey(parent)); + ssValue.Xor(dbwrapper_private::GetObfuscation(parent)); leveldb::Slice slValue(CharCast(ssValue.data()), ssValue.size()); m_impl_batch->batch.Put(slKey, slValue); } @@ -250,23 +250,23 @@ CDBWrapper::CDBWrapper(const DBParams& params) } // The base-case obfuscation key, which is a noop. - obfuscate_key = std::vector(Obfuscation::KEY_SIZE, '\000'); + m_obfuscation = std::vector(Obfuscation::KEY_SIZE, '\000'); - bool key_exists = Read(OBFUSCATE_KEY_KEY, obfuscate_key); + bool key_exists = Read(OBFUSCATION_KEY_KEY, m_obfuscation); if (!key_exists && params.obfuscate && IsEmpty()) { // Initialize non-degenerate obfuscation if it won't upset // existing, non-obfuscated data. - std::vector new_key = CreateObfuscateKey(); + std::vector new_key = CreateObfuscation(); // Write `new_key` so we don't obfuscate the key with itself - Write(OBFUSCATE_KEY_KEY, new_key); - obfuscate_key = new_key; + Write(OBFUSCATION_KEY_KEY, new_key); + m_obfuscation = new_key; - LogPrintf("Wrote new obfuscate key for %s: %s\n", fs::PathToString(params.path), HexStr(obfuscate_key)); + LogPrintf("Wrote new obfuscation key for %s: %s\n", fs::PathToString(params.path), HexStr(m_obfuscation)); } - LogPrintf("Using obfuscation key for %s: %s\n", fs::PathToString(params.path), HexStr(obfuscate_key)); + LogPrintf("Using obfuscation key for %s: %s\n", fs::PathToString(params.path), HexStr(m_obfuscation)); } CDBWrapper::~CDBWrapper() @@ -315,13 +315,13 @@ size_t CDBWrapper::DynamicMemoryUsage() const // // We must use a string constructor which specifies length so that we copy // past the null-terminator. -const std::string CDBWrapper::OBFUSCATE_KEY_KEY("\000obfuscate_key", 14); +const std::string CDBWrapper::OBFUSCATION_KEY_KEY("\000obfuscate_key", 14); /** * Returns a string (consisting of 8 random bytes) suitable for use as an * obfuscating XOR key. */ -std::vector CDBWrapper::CreateObfuscateKey() const +std::vector CDBWrapper::CreateObfuscation() const { std::vector ret(Obfuscation::KEY_SIZE); GetRandBytes(ret); @@ -411,9 +411,9 @@ void CDBIterator::Next() { m_impl_iter->iter->Next(); } namespace dbwrapper_private { -const std::vector& GetObfuscateKey(const CDBWrapper &w) +const std::vector& GetObfuscation(const CDBWrapper &w) { - return w.obfuscate_key; + return w.m_obfuscation; } } // namespace dbwrapper_private diff --git a/src/dbwrapper.h b/src/dbwrapper.h index 64d428ce5de..88684cab175 100644 --- a/src/dbwrapper.h +++ b/src/dbwrapper.h @@ -63,7 +63,7 @@ namespace dbwrapper_private { * Database obfuscation should be considered an implementation detail of the * specific database. */ -const std::vector& GetObfuscateKey(const CDBWrapper &w); +const std::vector& GetObfuscation(const CDBWrapper &w); }; // namespace dbwrapper_private @@ -166,7 +166,7 @@ public: template bool GetValue(V& value) { try { DataStream ssValue{GetValueImpl()}; - ssValue.Xor(dbwrapper_private::GetObfuscateKey(parent)); + ssValue.Xor(dbwrapper_private::GetObfuscation(parent)); ssValue >> value; } catch (const std::exception&) { return false; @@ -179,7 +179,7 @@ struct LevelDBContext; class CDBWrapper { - friend const std::vector& dbwrapper_private::GetObfuscateKey(const CDBWrapper &w); + friend const std::vector& dbwrapper_private::GetObfuscation(const CDBWrapper &w); private: //! holds all leveldb-specific fields of this class std::unique_ptr m_db_context; @@ -188,12 +188,12 @@ private: std::string m_name; //! a key used for optional XOR-obfuscation of the database - std::vector obfuscate_key; + std::vector m_obfuscation; //! the key under which the obfuscation key is stored - static const std::string OBFUSCATE_KEY_KEY; + static const std::string OBFUSCATION_KEY_KEY; - std::vector CreateObfuscateKey() const; + std::vector CreateObfuscation() const; //! path to filesystem storage const fs::path m_path; @@ -225,7 +225,7 @@ public: } try { DataStream ssValue{MakeByteSpan(*strValue)}; - ssValue.Xor(obfuscate_key); + ssValue.Xor(m_obfuscation); ssValue >> value; } catch (const std::exception&) { return false; diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index e224c5985d8..53a2fd14c38 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -780,13 +780,13 @@ void BlockManager::UnlinkPrunedFiles(const std::set& setFilesToPrune) const AutoFile BlockManager::OpenBlockFile(const FlatFilePos& pos, bool fReadOnly) const { - return AutoFile{m_block_file_seq.Open(pos, fReadOnly), m_xor_key}; + return AutoFile{m_block_file_seq.Open(pos, fReadOnly), m_obfuscation}; } /** Open an undo file (rev?????.dat) */ AutoFile BlockManager::OpenUndoFile(const FlatFilePos& pos, bool fReadOnly) const { - return AutoFile{m_undo_file_seq.Open(pos, fReadOnly), m_xor_key}; + return AutoFile{m_undo_file_seq.Open(pos, fReadOnly), m_obfuscation}; } fs::path BlockManager::GetBlockPosFilename(const FlatFilePos& pos) const @@ -1124,7 +1124,7 @@ static auto InitBlocksdirXorKey(const BlockManager::Options& opts) { // Bytes are serialized without length indicator, so this is also the exact // size of the XOR-key file. - std::array xor_key{}; + std::array obfuscation{}; // Consider this to be the first run if the blocksdir contains only hidden // files (those which start with a .). Checking for a fully-empty dir would @@ -1141,14 +1141,14 @@ static auto InitBlocksdirXorKey(const BlockManager::Options& opts) if (opts.use_xor && first_run) { // Only use random fresh key when the boolean option is set and on the // very first start of the program. - FastRandomContext{}.fillrand(xor_key); + FastRandomContext{}.fillrand(obfuscation); } const fs::path xor_key_path{opts.blocks_dir / "xor.dat"}; if (fs::exists(xor_key_path)) { // A pre-existing xor key file has priority. AutoFile xor_key_file{fsbridge::fopen(xor_key_path, "rb")}; - xor_key_file >> xor_key; + xor_key_file >> obfuscation; } else { // Create initial or missing xor key file AutoFile xor_key_file{fsbridge::fopen(xor_key_path, @@ -1158,7 +1158,7 @@ static auto InitBlocksdirXorKey(const BlockManager::Options& opts) "wbx" #endif )}; - xor_key_file << xor_key; + xor_key_file << obfuscation; if (xor_key_file.fclose() != 0) { throw std::runtime_error{strprintf("Error closing XOR key file %s: %s", fs::PathToString(xor_key_path), @@ -1166,20 +1166,20 @@ static auto InitBlocksdirXorKey(const BlockManager::Options& opts) } } // If the user disabled the key, it must be zero. - if (!opts.use_xor && xor_key != decltype(xor_key){}) { + if (!opts.use_xor && obfuscation != decltype(obfuscation){}) { throw std::runtime_error{ strprintf("The blocksdir XOR-key can not be disabled when a random key was already stored! " "Stored key: '%s', stored path: '%s'.", - HexStr(xor_key), fs::PathToString(xor_key_path)), + HexStr(obfuscation), fs::PathToString(xor_key_path)), }; } - LogInfo("Using obfuscation key for blocksdir *.dat files (%s): '%s'\n", fs::PathToString(opts.blocks_dir), HexStr(xor_key)); - return std::vector{xor_key.begin(), xor_key.end()}; + LogInfo("Using obfuscation key for blocksdir *.dat files (%s): '%s'\n", fs::PathToString(opts.blocks_dir), HexStr(obfuscation)); + return std::vector{obfuscation.begin(), obfuscation.end()}; } BlockManager::BlockManager(const util::SignalInterrupt& interrupt, Options opts) : m_prune_mode{opts.prune_target > 0}, - m_xor_key{InitBlocksdirXorKey(opts)}, + m_obfuscation{InitBlocksdirXorKey(opts)}, m_opts{std::move(opts)}, m_block_file_seq{FlatFileSeq{m_opts.blocks_dir, "blk", m_opts.fast_prune ? 0x4000 /* 16kB */ : BLOCKFILE_CHUNK_SIZE}}, m_undo_file_seq{FlatFileSeq{m_opts.blocks_dir, "rev", UNDOFILE_CHUNK_SIZE}}, diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index f9a03eaf83b..5c3a5f024b8 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -235,7 +235,7 @@ private: const bool m_prune_mode; - const std::vector m_xor_key; + const std::vector m_obfuscation; /** Dirty block index entries. */ std::set m_dirty_blockindex; diff --git a/src/node/mempool_persist.cpp b/src/node/mempool_persist.cpp index eeb690b0877..9054fd7bb88 100644 --- a/src/node/mempool_persist.cpp +++ b/src/node/mempool_persist.cpp @@ -60,15 +60,15 @@ bool LoadMempool(CTxMemPool& pool, const fs::path& load_path, Chainstate& active try { uint64_t version; file >> version; - std::vector xor_key; + std::vector obfuscation; if (version == MEMPOOL_DUMP_VERSION_NO_XOR_KEY) { // Leave XOR-key empty } else if (version == MEMPOOL_DUMP_VERSION) { - file >> xor_key; + file >> obfuscation; } else { return false; } - file.SetXor(xor_key); + file.SetObfuscation(obfuscation); uint64_t total_txns_to_load; file >> total_txns_to_load; uint64_t txns_tried = 0; @@ -180,12 +180,12 @@ bool DumpMempool(const CTxMemPool& pool, const fs::path& dump_path, FopenFn mock const uint64_t version{pool.m_opts.persist_v1_dat ? MEMPOOL_DUMP_VERSION_NO_XOR_KEY : MEMPOOL_DUMP_VERSION}; file << version; - std::vector xor_key(Obfuscation::KEY_SIZE); + std::vector obfuscation(Obfuscation::KEY_SIZE); if (!pool.m_opts.persist_v1_dat) { - FastRandomContext{}.fillrand(xor_key); - file << xor_key; + FastRandomContext{}.fillrand(obfuscation); + file << obfuscation; } - file.SetXor(xor_key); + file.SetObfuscation(obfuscation); uint64_t mempool_transactions_to_write(vinfo.size()); file << mempool_transactions_to_write; diff --git a/src/streams.cpp b/src/streams.cpp index 2c873e50d3b..ed80d11dcee 100644 --- a/src/streams.cpp +++ b/src/streams.cpp @@ -9,8 +9,8 @@ #include -AutoFile::AutoFile(std::FILE* file, std::vector data_xor) - : m_file{file}, m_xor{std::move(data_xor)} +AutoFile::AutoFile(std::FILE* file, std::vector obfuscation) + : m_file{file}, m_obfuscation{std::move(obfuscation)} { if (!IsNull()) { auto pos{std::ftell(m_file)}; @@ -22,9 +22,9 @@ std::size_t AutoFile::detail_fread(std::span dst) { if (!m_file) throw std::ios_base::failure("AutoFile::read: file handle is nullptr"); size_t ret = std::fread(dst.data(), 1, dst.size(), m_file); - if (!m_xor.empty()) { + if (!m_obfuscation.empty()) { if (!m_position.has_value()) throw std::ios_base::failure("AutoFile::read: position unknown"); - util::Xor(dst.subspan(0, ret), m_xor, *m_position); + util::Xor(dst.subspan(0, ret), m_obfuscation, *m_position); } if (m_position.has_value()) *m_position += ret; return ret; @@ -81,7 +81,7 @@ void AutoFile::ignore(size_t nSize) void AutoFile::write(std::span src) { if (!m_file) throw std::ios_base::failure("AutoFile::write: file handle is nullptr"); - if (m_xor.empty()) { + if (m_obfuscation.empty()) { if (std::fwrite(src.data(), 1, src.size(), m_file) != src.size()) { throw std::ios_base::failure("AutoFile::write: write failed"); } @@ -101,9 +101,9 @@ void AutoFile::write(std::span src) void AutoFile::write_buffer(std::span src) { if (!m_file) throw std::ios_base::failure("AutoFile::write_buffer: file handle is nullptr"); - if (m_xor.size()) { + if (m_obfuscation.size()) { if (!m_position) throw std::ios_base::failure("AutoFile::write_buffer: obfuscation position unknown"); - util::Xor(src, m_xor, *m_position); // obfuscate in-place + util::Xor(src, m_obfuscation, *m_position); // obfuscate in-place } if (std::fwrite(src.data(), 1, src.size(), m_file) != src.size()) { throw std::ios_base::failure("AutoFile::write_buffer: write failed"); diff --git a/src/streams.h b/src/streams.h index ae9cb94bae3..ac6d5a0de16 100644 --- a/src/streams.h +++ b/src/streams.h @@ -402,12 +402,12 @@ class AutoFile { protected: std::FILE* m_file; - std::vector m_xor; + std::vector m_obfuscation; std::optional m_position; bool m_was_written{false}; public: - explicit AutoFile(std::FILE* file, std::vector data_xor={}); + explicit AutoFile(std::FILE* file, std::vector obfuscation={}); ~AutoFile() { @@ -455,7 +455,7 @@ public: bool IsNull() const { return m_file == nullptr; } /** Continue with a different XOR key */ - void SetXor(std::vector data_xor) { m_xor = data_xor; } + void SetObfuscation(std::vector obfuscation) { m_obfuscation = obfuscation; } /** Implementation detail, only used internally. */ std::size_t detail_fread(std::span dst); diff --git a/src/test/dbwrapper_tests.cpp b/src/test/dbwrapper_tests.cpp index bd50db17c13..9c723351d6b 100644 --- a/src/test/dbwrapper_tests.cpp +++ b/src/test/dbwrapper_tests.cpp @@ -42,7 +42,7 @@ BOOST_AUTO_TEST_CASE(dbwrapper) BOOST_CHECK_EQUAL(obfuscate, !dbw.IsEmpty()); // Ensure that we're doing real obfuscation when obfuscate=true - obfuscation_key = dbwrapper_private::GetObfuscateKey(dbw); + obfuscation_key = dbwrapper_private::GetObfuscation(dbw); BOOST_CHECK_EQUAL(obfuscate, !is_null_key(obfuscation_key)); for (uint8_t k{0}; k < 10; ++k) { @@ -56,7 +56,7 @@ BOOST_AUTO_TEST_CASE(dbwrapper) // Verify that the obfuscation key is never obfuscated { CDBWrapper dbw{{.path = path, .cache_bytes = CACHE_SIZE, .obfuscate = false}}; - BOOST_CHECK(obfuscation_key == dbwrapper_private::GetObfuscateKey(dbw)); + BOOST_CHECK(obfuscation_key == dbwrapper_private::GetObfuscation(dbw)); } // Read back the values @@ -64,7 +64,7 @@ BOOST_AUTO_TEST_CASE(dbwrapper) CDBWrapper dbw{{.path = path, .cache_bytes = CACHE_SIZE, .obfuscate = obfuscate}}; // Ensure obfuscation is read back correctly - BOOST_CHECK(obfuscation_key == dbwrapper_private::GetObfuscateKey(dbw)); + BOOST_CHECK(obfuscation_key == dbwrapper_private::GetObfuscation(dbw)); BOOST_CHECK_EQUAL(obfuscate, !is_null_key(obfuscation_key)); // Verify all written values @@ -89,7 +89,7 @@ BOOST_AUTO_TEST_CASE(dbwrapper_basic_data) bool res_bool; // Ensure that we're doing real obfuscation when obfuscate=true - BOOST_CHECK_EQUAL(obfuscate, !is_null_key(dbwrapper_private::GetObfuscateKey(dbw))); + BOOST_CHECK_EQUAL(obfuscate, !is_null_key(dbwrapper_private::GetObfuscation(dbw))); //Simulate block raw data - "b + block hash" std::string key_block = "b" + m_rng.rand256().ToString(); @@ -264,7 +264,7 @@ BOOST_AUTO_TEST_CASE(existing_data_no_obfuscate) BOOST_CHECK_EQUAL(res2.ToString(), in.ToString()); BOOST_CHECK(!odbw.IsEmpty()); - BOOST_CHECK(is_null_key(dbwrapper_private::GetObfuscateKey(odbw))); // The key should be an empty string + BOOST_CHECK(is_null_key(dbwrapper_private::GetObfuscation(odbw))); // The key should be an empty string uint256 in2 = m_rng.rand256(); uint256 res3; @@ -301,7 +301,7 @@ BOOST_AUTO_TEST_CASE(existing_data_reindex) // Check that the key/val we wrote with unobfuscated wrapper doesn't exist uint256 res2; BOOST_CHECK(!odbw.Read(key, res2)); - BOOST_CHECK(!is_null_key(dbwrapper_private::GetObfuscateKey(odbw))); + BOOST_CHECK(!is_null_key(dbwrapper_private::GetObfuscation(odbw))); uint256 in2 = m_rng.rand256(); uint256 res3; diff --git a/src/test/streams_tests.cpp b/src/test/streams_tests.cpp index 9c1e1032a0f..5da1ff99154 100644 --- a/src/test/streams_tests.cpp +++ b/src/test/streams_tests.cpp @@ -79,11 +79,11 @@ BOOST_AUTO_TEST_CASE(xor_file) auto raw_file{[&](const auto& mode) { return fsbridge::fopen(xor_path, mode); }}; const std::vector test1{1, 2, 3}; const std::vector test2{4, 5}; - const auto xor_pat{"ff00ff00ff00ff00"_hex_v}; + const auto obfuscation{"ff00ff00ff00ff00"_hex_v}; { // Check errors for missing file - AutoFile xor_file{raw_file("rb"), xor_pat}; + AutoFile xor_file{raw_file("rb"), obfuscation}; BOOST_CHECK_EXCEPTION(xor_file << std::byte{}, std::ios_base::failure, HasReason{"AutoFile::write: file handle is nullptr"}); BOOST_CHECK_EXCEPTION(xor_file >> std::byte{}, std::ios_base::failure, HasReason{"AutoFile::read: file handle is nullptr"}); BOOST_CHECK_EXCEPTION(xor_file.ignore(1), std::ios_base::failure, HasReason{"AutoFile::ignore: file handle is nullptr"}); @@ -95,7 +95,7 @@ BOOST_AUTO_TEST_CASE(xor_file) #else const char* mode = "wbx"; #endif - AutoFile xor_file{raw_file(mode), xor_pat}; + AutoFile xor_file{raw_file(mode), obfuscation}; xor_file << test1 << test2; BOOST_REQUIRE_EQUAL(xor_file.fclose(), 0); } @@ -109,7 +109,7 @@ BOOST_AUTO_TEST_CASE(xor_file) BOOST_CHECK_EXCEPTION(non_xor_file.ignore(1), std::ios_base::failure, HasReason{"AutoFile::ignore: end of file"}); } { - AutoFile xor_file{raw_file("rb"), xor_pat}; + AutoFile xor_file{raw_file("rb"), obfuscation}; std::vector read1, read2; xor_file >> read1 >> read2; BOOST_CHECK_EQUAL(HexStr(read1), HexStr(test1)); @@ -118,7 +118,7 @@ BOOST_AUTO_TEST_CASE(xor_file) BOOST_CHECK_EXCEPTION(xor_file >> std::byte{}, std::ios_base::failure, HasReason{"AutoFile::read: end of file"}); } { - AutoFile xor_file{raw_file("rb"), xor_pat}; + AutoFile xor_file{raw_file("rb"), obfuscation}; std::vector read2; // Check that ignore works xor_file.ignore(4); From 6bbf2d9311b47a8a15c17d9fe11828ee623d98e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sat, 5 Apr 2025 18:25:20 +0200 Subject: [PATCH 07/12] refactor: prepare `DBWrapper` for obfuscation key change Since `FastRandomContext` delegates to `GetRandBytes` anyway, we can simplify new key generation to a Write/Read combo, unifying the flow of enabling obfuscation via `Read`. The comments were also adjusted to clarify that the `m_obfuscation` field affects the behavior of `Read` and `Write` methods. These changes are meant to simplify the diffs for the riskier optimization commits later. --- src/dbwrapper.cpp | 40 +++++++--------------------------------- src/dbwrapper.h | 6 ++---- 2 files changed, 9 insertions(+), 37 deletions(-) diff --git a/src/dbwrapper.cpp b/src/dbwrapper.cpp index 45f806705b4..f92c9b4cf62 100644 --- a/src/dbwrapper.cpp +++ b/src/dbwrapper.cpp @@ -249,24 +249,15 @@ CDBWrapper::CDBWrapper(const DBParams& params) LogPrintf("Finished database compaction of %s\n", fs::PathToString(params.path)); } - // The base-case obfuscation key, which is a noop. - m_obfuscation = std::vector(Obfuscation::KEY_SIZE, '\000'); - - bool key_exists = Read(OBFUSCATION_KEY_KEY, m_obfuscation); - - if (!key_exists && params.obfuscate && IsEmpty()) { - // Initialize non-degenerate obfuscation if it won't upset - // existing, non-obfuscated data. - std::vector new_key = CreateObfuscation(); - - // Write `new_key` so we don't obfuscate the key with itself - Write(OBFUSCATION_KEY_KEY, new_key); - m_obfuscation = new_key; - - LogPrintf("Wrote new obfuscation key for %s: %s\n", fs::PathToString(params.path), HexStr(m_obfuscation)); + m_obfuscation = std::vector(Obfuscation::KEY_SIZE, '\000'); // Needed for unobfuscated Read()/Write() below + if (!Read(OBFUSCATION_KEY_KEY, m_obfuscation) && params.obfuscate && IsEmpty()) { + // Generate, write and read back the new obfuscation key, making sure we don't obfuscate the key itself + Write(OBFUSCATION_KEY_KEY, FastRandomContext{}.randbytes(Obfuscation::KEY_SIZE)); + Read(OBFUSCATION_KEY_KEY, m_obfuscation); + LogInfo("Wrote new obfuscation key for %s: %s", fs::PathToString(params.path), HexStr(m_obfuscation)); } + LogInfo("Using obfuscation key for %s: %s", fs::PathToString(params.path), HexStr(m_obfuscation)); - LogPrintf("Using obfuscation key for %s: %s\n", fs::PathToString(params.path), HexStr(m_obfuscation)); } CDBWrapper::~CDBWrapper() @@ -311,23 +302,6 @@ size_t CDBWrapper::DynamicMemoryUsage() const return parsed.value(); } -// Prefixed with null character to avoid collisions with other keys -// -// We must use a string constructor which specifies length so that we copy -// past the null-terminator. -const std::string CDBWrapper::OBFUSCATION_KEY_KEY("\000obfuscate_key", 14); - -/** - * Returns a string (consisting of 8 random bytes) suitable for use as an - * obfuscating XOR key. - */ -std::vector CDBWrapper::CreateObfuscation() const -{ - std::vector ret(Obfuscation::KEY_SIZE); - GetRandBytes(ret); - return ret; -} - std::optional CDBWrapper::ReadImpl(std::span key) const { leveldb::Slice slKey(CharCast(key.data()), key.size()); diff --git a/src/dbwrapper.h b/src/dbwrapper.h index 88684cab175..0935320ecac 100644 --- a/src/dbwrapper.h +++ b/src/dbwrapper.h @@ -190,10 +190,8 @@ private: //! a key used for optional XOR-obfuscation of the database std::vector m_obfuscation; - //! the key under which the obfuscation key is stored - static const std::string OBFUSCATION_KEY_KEY; - - std::vector CreateObfuscation() const; + //! obfuscation key storage key, null-prefixed to avoid collisions + inline static const std::string OBFUSCATION_KEY_KEY{"\000obfuscate_key", 14}; // explicit size to avoid truncation at leading \0 //! path to filesystem storage const fs::path m_path; From fa5d296e3beb312e2bc39532a12bcdf187c6da91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sat, 5 Apr 2025 19:01:09 +0200 Subject: [PATCH 08/12] refactor: prepare mempool_persist for obfuscation key change These changes are meant to simplify the diffs for the riskier optimization commits later. --- src/node/mempool_persist.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/node/mempool_persist.cpp b/src/node/mempool_persist.cpp index 9054fd7bb88..eac8d386e74 100644 --- a/src/node/mempool_persist.cpp +++ b/src/node/mempool_persist.cpp @@ -60,15 +60,17 @@ bool LoadMempool(CTxMemPool& pool, const fs::path& load_path, Chainstate& active try { uint64_t version; file >> version; - std::vector obfuscation; + if (version == MEMPOOL_DUMP_VERSION_NO_XOR_KEY) { - // Leave XOR-key empty + file.SetObfuscation({}); } else if (version == MEMPOOL_DUMP_VERSION) { + std::vector obfuscation(Obfuscation::KEY_SIZE); file >> obfuscation; + file.SetObfuscation(obfuscation); } else { return false; } - file.SetObfuscation(obfuscation); + uint64_t total_txns_to_load; file >> total_txns_to_load; uint64_t txns_tried = 0; @@ -180,12 +182,14 @@ bool DumpMempool(const CTxMemPool& pool, const fs::path& dump_path, FopenFn mock const uint64_t version{pool.m_opts.persist_v1_dat ? MEMPOOL_DUMP_VERSION_NO_XOR_KEY : MEMPOOL_DUMP_VERSION}; file << version; - std::vector obfuscation(Obfuscation::KEY_SIZE); if (!pool.m_opts.persist_v1_dat) { + std::vector obfuscation(Obfuscation::KEY_SIZE); FastRandomContext{}.fillrand(obfuscation); file << obfuscation; + file.SetObfuscation(obfuscation); + } else { + file.SetObfuscation({}); } - file.SetObfuscation(obfuscation); uint64_t mempool_transactions_to_write(vinfo.size()); file << mempool_transactions_to_write; From 377aab8e5a8da2ea20383b4dde59094cc42d3407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sat, 5 Jul 2025 13:39:15 +0200 Subject: [PATCH 09/12] refactor: move `util::Xor` to `Obfuscation().Xor` This is meant to focus the usages to narrow the scope of the obfuscation optimization. `Obfuscation::Xor` is mostly a move. Co-authored-by: maflcko <6399679+maflcko@users.noreply.github.com> --- src/bench/obfuscation.cpp | 3 +-- src/streams.cpp | 5 +++-- src/streams.h | 24 ++---------------------- src/test/streams_tests.cpp | 4 ++-- src/util/obfuscation.h | 18 ++++++++++++++++++ 5 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/bench/obfuscation.cpp b/src/bench/obfuscation.cpp index 27a254f8037..2e9f9a453af 100644 --- a/src/bench/obfuscation.cpp +++ b/src/bench/obfuscation.cpp @@ -4,7 +4,6 @@ #include #include -#include #include #include @@ -18,7 +17,7 @@ static void ObfuscationBench(benchmark::Bench& bench) size_t offset{0}; bench.batch(data.size()).unit("byte").run([&] { - util::Xor(data, key, offset++); // mutated differently each time + Obfuscation().Xor(data, key, offset++); // mutated differently each time ankerl::nanobench::doNotOptimizeAway(data); }); } diff --git a/src/streams.cpp b/src/streams.cpp index ed80d11dcee..b33a1288879 100644 --- a/src/streams.cpp +++ b/src/streams.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -24,7 +25,7 @@ std::size_t AutoFile::detail_fread(std::span dst) size_t ret = std::fread(dst.data(), 1, dst.size(), m_file); if (!m_obfuscation.empty()) { if (!m_position.has_value()) throw std::ios_base::failure("AutoFile::read: position unknown"); - util::Xor(dst.subspan(0, ret), m_obfuscation, *m_position); + Obfuscation().Xor(dst.subspan(0, ret), m_obfuscation, *m_position); } if (m_position.has_value()) *m_position += ret; return ret; @@ -103,7 +104,7 @@ void AutoFile::write_buffer(std::span src) if (!m_file) throw std::ios_base::failure("AutoFile::write_buffer: file handle is nullptr"); if (m_obfuscation.size()) { if (!m_position) throw std::ios_base::failure("AutoFile::write_buffer: obfuscation position unknown"); - util::Xor(src, m_obfuscation, *m_position); // obfuscate in-place + Obfuscation().Xor(src, m_obfuscation, *m_position); // obfuscate in-place } if (std::fwrite(src.data(), 1, src.size(), m_file) != src.size()) { throw std::ios_base::failure("AutoFile::write_buffer: write failed"); diff --git a/src/streams.h b/src/streams.h index ac6d5a0de16..f2b455742bd 100644 --- a/src/streams.h +++ b/src/streams.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -27,27 +28,6 @@ #include #include -namespace util { -inline void Xor(std::span write, std::span key, size_t key_offset = 0) -{ - if (key.size() == 0) { - return; - } - key_offset %= key.size(); - - for (size_t i = 0, j = key_offset; i != write.size(); i++) { - write[i] ^= key[j++]; - - // This potentially acts on very many bytes of data, so it's - // important that we calculate `j`, i.e. the `key` index in this - // way instead of doing a %, which would effectively be a division - // for each byte Xor'd -- much slower than need be. - if (j == key.size()) - j = 0; - } -} -} // namespace util - /* Minimal stream for overwriting and/or appending to an existing byte vector * * The referenced vector will grow as necessary @@ -279,7 +259,7 @@ public: */ void Xor(const std::vector& key) { - util::Xor(MakeWritableByteSpan(*this), MakeByteSpan(key)); + Obfuscation().Xor(MakeWritableByteSpan(*this), MakeByteSpan(key)); } /** Compute total memory usage of this object (own memory + any dynamic memory). */ diff --git a/src/test/streams_tests.cpp b/src/test/streams_tests.cpp index 5da1ff99154..346497b3c31 100644 --- a/src/test/streams_tests.cpp +++ b/src/test/streams_tests.cpp @@ -24,7 +24,7 @@ BOOST_AUTO_TEST_CASE(xor_roundtrip_random_chunks) auto apply_random_xor_chunks{[&](std::span target, std::span obfuscation) { for (size_t offset{0}; offset < target.size();) { const size_t chunk_size{1 + m_rng.randrange(target.size() - offset)}; - util::Xor(target.subspan(offset, chunk_size), obfuscation, offset); + Obfuscation().Xor(target.subspan(offset, chunk_size), obfuscation, offset); offset += chunk_size; } }}; @@ -67,7 +67,7 @@ BOOST_AUTO_TEST_CASE(xor_bytes_reference) std::vector actual{expected}; expected_xor(std::span{expected}.subspan(write_offset), key_bytes, key_offset); - util::Xor(std::span{actual}.subspan(write_offset), key_bytes, key_offset); + Obfuscation().Xor(std::span{actual}.subspan(write_offset), key_bytes, key_offset); BOOST_CHECK_EQUAL_COLLECTIONS(expected.begin(), expected.end(), actual.begin(), actual.end()); } diff --git a/src/util/obfuscation.h b/src/util/obfuscation.h index 628dacfc9d5..c39d7bf80a5 100644 --- a/src/util/obfuscation.h +++ b/src/util/obfuscation.h @@ -6,11 +6,29 @@ #define BITCOIN_UTIL_OBFUSCATION_H #include +#include class Obfuscation { public: static constexpr size_t KEY_SIZE{sizeof(uint64_t)}; + + void Xor(std::span write, std::span key, size_t key_offset = 0) + { + assert(key.size() == KEY_SIZE); + key_offset %= KEY_SIZE; + + for (size_t i = 0, j = key_offset; i != write.size(); i++) { + write[i] ^= key[j++]; + + // This potentially acts on very many bytes of data, so it's + // important that we calculate `j`, i.e. the `key` index in this + // way instead of doing a %, which would effectively be a division + // for each byte Xor'd -- much slower than need be. + if (j == KEY_SIZE) + j = 0; + } + } }; #endif // BITCOIN_UTIL_OBFUSCATION_H From 478d40afc6faaca47b5cf94bb461692d03347599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sat, 5 Jul 2025 14:39:19 +0200 Subject: [PATCH 10/12] refactor: encapsulate `vector`/`array` keys into `Obfuscation` --- src/bench/obfuscation.cpp | 4 +-- src/dbwrapper.cpp | 11 +++--- src/dbwrapper.h | 14 ++++---- src/node/blockstorage.cpp | 2 +- src/node/blockstorage.h | 2 +- src/node/mempool_persist.cpp | 5 ++- src/streams.cpp | 19 +++++----- src/streams.h | 19 +++------- src/test/dbwrapper_tests.cpp | 29 ++++++--------- src/test/fuzz/autofile.cpp | 2 +- src/test/fuzz/buffered_file.cpp | 2 +- src/test/streams_tests.cpp | 63 +++++++++++++++++++++++++-------- src/util/obfuscation.h | 49 ++++++++++++++++++++++--- 13 files changed, 136 insertions(+), 85 deletions(-) diff --git a/src/bench/obfuscation.cpp b/src/bench/obfuscation.cpp index 2e9f9a453af..178be56a5d5 100644 --- a/src/bench/obfuscation.cpp +++ b/src/bench/obfuscation.cpp @@ -13,11 +13,11 @@ static void ObfuscationBench(benchmark::Bench& bench) { FastRandomContext frc{/*fDeterministic=*/true}; auto data{frc.randbytes(1024)}; - const auto key{frc.randbytes()}; + const Obfuscation obfuscation{frc.randbytes()}; size_t offset{0}; bench.batch(data.size()).unit("byte").run([&] { - Obfuscation().Xor(data, key, offset++); // mutated differently each time + obfuscation(data, offset++); // mutated differently each time ankerl::nanobench::doNotOptimizeAway(data); }); } diff --git a/src/dbwrapper.cpp b/src/dbwrapper.cpp index f92c9b4cf62..b13699572c4 100644 --- a/src/dbwrapper.cpp +++ b/src/dbwrapper.cpp @@ -174,7 +174,7 @@ void CDBBatch::Clear() void CDBBatch::WriteImpl(std::span key, DataStream& ssValue) { leveldb::Slice slKey(CharCast(key.data()), key.size()); - ssValue.Xor(dbwrapper_private::GetObfuscation(parent)); + dbwrapper_private::GetObfuscation(parent)(ssValue); leveldb::Slice slValue(CharCast(ssValue.data()), ssValue.size()); m_impl_batch->batch.Put(slKey, slValue); } @@ -249,15 +249,14 @@ CDBWrapper::CDBWrapper(const DBParams& params) LogPrintf("Finished database compaction of %s\n", fs::PathToString(params.path)); } - m_obfuscation = std::vector(Obfuscation::KEY_SIZE, '\000'); // Needed for unobfuscated Read()/Write() below + assert(!m_obfuscation); // Needed for unobfuscated Read()/Write() below if (!Read(OBFUSCATION_KEY_KEY, m_obfuscation) && params.obfuscate && IsEmpty()) { // Generate, write and read back the new obfuscation key, making sure we don't obfuscate the key itself Write(OBFUSCATION_KEY_KEY, FastRandomContext{}.randbytes(Obfuscation::KEY_SIZE)); Read(OBFUSCATION_KEY_KEY, m_obfuscation); - LogInfo("Wrote new obfuscation key for %s: %s", fs::PathToString(params.path), HexStr(m_obfuscation)); + LogInfo("Wrote new obfuscation key for %s: %s", fs::PathToString(params.path), m_obfuscation.HexKey()); } - LogInfo("Using obfuscation key for %s: %s", fs::PathToString(params.path), HexStr(m_obfuscation)); - + LogInfo("Using obfuscation key for %s: %s", fs::PathToString(params.path), m_obfuscation.HexKey()); } CDBWrapper::~CDBWrapper() @@ -385,7 +384,7 @@ void CDBIterator::Next() { m_impl_iter->iter->Next(); } namespace dbwrapper_private { -const std::vector& GetObfuscation(const CDBWrapper &w) +const Obfuscation& GetObfuscation(const CDBWrapper& w) { return w.m_obfuscation; } diff --git a/src/dbwrapper.h b/src/dbwrapper.h index 0935320ecac..c5d49404861 100644 --- a/src/dbwrapper.h +++ b/src/dbwrapper.h @@ -18,7 +18,6 @@ #include #include #include -#include static const size_t DBWRAPPER_PREALLOC_KEY_SIZE = 64; static const size_t DBWRAPPER_PREALLOC_VALUE_SIZE = 1024; @@ -63,8 +62,7 @@ namespace dbwrapper_private { * Database obfuscation should be considered an implementation detail of the * specific database. */ -const std::vector& GetObfuscation(const CDBWrapper &w); - +const Obfuscation& GetObfuscation(const CDBWrapper&); }; // namespace dbwrapper_private bool DestroyDB(const std::string& path_str); @@ -166,7 +164,7 @@ public: template bool GetValue(V& value) { try { DataStream ssValue{GetValueImpl()}; - ssValue.Xor(dbwrapper_private::GetObfuscation(parent)); + dbwrapper_private::GetObfuscation(parent)(ssValue); ssValue >> value; } catch (const std::exception&) { return false; @@ -179,7 +177,7 @@ struct LevelDBContext; class CDBWrapper { - friend const std::vector& dbwrapper_private::GetObfuscation(const CDBWrapper &w); + friend const Obfuscation& dbwrapper_private::GetObfuscation(const CDBWrapper&); private: //! holds all leveldb-specific fields of this class std::unique_ptr m_db_context; @@ -187,8 +185,8 @@ private: //! the name of this database std::string m_name; - //! a key used for optional XOR-obfuscation of the database - std::vector m_obfuscation; + //! optional XOR-obfuscation of the database + Obfuscation m_obfuscation; //! obfuscation key storage key, null-prefixed to avoid collisions inline static const std::string OBFUSCATION_KEY_KEY{"\000obfuscate_key", 14}; // explicit size to avoid truncation at leading \0 @@ -223,7 +221,7 @@ public: } try { DataStream ssValue{MakeByteSpan(*strValue)}; - ssValue.Xor(m_obfuscation); + m_obfuscation(ssValue); ssValue >> value; } catch (const std::exception&) { return false; diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 53a2fd14c38..4f6a4977bdb 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -1174,7 +1174,7 @@ static auto InitBlocksdirXorKey(const BlockManager::Options& opts) }; } LogInfo("Using obfuscation key for blocksdir *.dat files (%s): '%s'\n", fs::PathToString(opts.blocks_dir), HexStr(obfuscation)); - return std::vector{obfuscation.begin(), obfuscation.end()}; + return Obfuscation{obfuscation}; } BlockManager::BlockManager(const util::SignalInterrupt& interrupt, Options opts) diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index 5c3a5f024b8..cee0eb61ed6 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -235,7 +235,7 @@ private: const bool m_prune_mode; - const std::vector m_obfuscation; + const Obfuscation m_obfuscation; /** Dirty block index entries. */ std::set m_dirty_blockindex; diff --git a/src/node/mempool_persist.cpp b/src/node/mempool_persist.cpp index eac8d386e74..5b0f80be6c8 100644 --- a/src/node/mempool_persist.cpp +++ b/src/node/mempool_persist.cpp @@ -64,7 +64,7 @@ bool LoadMempool(CTxMemPool& pool, const fs::path& load_path, Chainstate& active if (version == MEMPOOL_DUMP_VERSION_NO_XOR_KEY) { file.SetObfuscation({}); } else if (version == MEMPOOL_DUMP_VERSION) { - std::vector obfuscation(Obfuscation::KEY_SIZE); + Obfuscation obfuscation; file >> obfuscation; file.SetObfuscation(obfuscation); } else { @@ -183,8 +183,7 @@ bool DumpMempool(const CTxMemPool& pool, const fs::path& dump_path, FopenFn mock file << version; if (!pool.m_opts.persist_v1_dat) { - std::vector obfuscation(Obfuscation::KEY_SIZE); - FastRandomContext{}.fillrand(obfuscation); + const Obfuscation obfuscation{FastRandomContext{}.randbytes()}; file << obfuscation; file.SetObfuscation(obfuscation); } else { diff --git a/src/streams.cpp b/src/streams.cpp index b33a1288879..0364c2134f4 100644 --- a/src/streams.cpp +++ b/src/streams.cpp @@ -10,8 +10,7 @@ #include -AutoFile::AutoFile(std::FILE* file, std::vector obfuscation) - : m_file{file}, m_obfuscation{std::move(obfuscation)} +AutoFile::AutoFile(std::FILE* file, const Obfuscation& obfuscation) : m_file{file}, m_obfuscation{obfuscation} { if (!IsNull()) { auto pos{std::ftell(m_file)}; @@ -22,12 +21,12 @@ AutoFile::AutoFile(std::FILE* file, std::vector obfuscation) std::size_t AutoFile::detail_fread(std::span dst) { if (!m_file) throw std::ios_base::failure("AutoFile::read: file handle is nullptr"); - size_t ret = std::fread(dst.data(), 1, dst.size(), m_file); - if (!m_obfuscation.empty()) { - if (!m_position.has_value()) throw std::ios_base::failure("AutoFile::read: position unknown"); - Obfuscation().Xor(dst.subspan(0, ret), m_obfuscation, *m_position); + const size_t ret = std::fread(dst.data(), 1, dst.size(), m_file); + if (m_obfuscation) { + if (!m_position) throw std::ios_base::failure("AutoFile::read: position unknown"); + m_obfuscation(dst.subspan(0, ret), *m_position); } - if (m_position.has_value()) *m_position += ret; + if (m_position) *m_position += ret; return ret; } @@ -82,7 +81,7 @@ void AutoFile::ignore(size_t nSize) void AutoFile::write(std::span src) { if (!m_file) throw std::ios_base::failure("AutoFile::write: file handle is nullptr"); - if (m_obfuscation.empty()) { + if (!m_obfuscation) { if (std::fwrite(src.data(), 1, src.size(), m_file) != src.size()) { throw std::ios_base::failure("AutoFile::write: write failed"); } @@ -102,9 +101,9 @@ void AutoFile::write(std::span src) void AutoFile::write_buffer(std::span src) { if (!m_file) throw std::ios_base::failure("AutoFile::write_buffer: file handle is nullptr"); - if (m_obfuscation.size()) { + if (m_obfuscation) { if (!m_position) throw std::ios_base::failure("AutoFile::write_buffer: obfuscation position unknown"); - Obfuscation().Xor(src, m_obfuscation, *m_position); // obfuscate in-place + m_obfuscation(src, *m_position); // obfuscate in-place } if (std::fwrite(src.data(), 1, src.size(), m_file) != src.size()) { throw std::ios_base::failure("AutoFile::write_buffer: write failed"); diff --git a/src/streams.h b/src/streams.h index f2b455742bd..36af8dd6159 100644 --- a/src/streams.h +++ b/src/streams.h @@ -25,7 +25,6 @@ #include #include #include -#include #include /* Minimal stream for overwriting and/or appending to an existing byte vector @@ -245,23 +244,13 @@ public: return (*this); } - template + template DataStream& operator>>(T&& obj) { ::Unserialize(*this, obj); return (*this); } - /** - * XOR the contents of this stream with a certain key. - * - * @param[in] key The key used to XOR the data in this stream. - */ - void Xor(const std::vector& key) - { - Obfuscation().Xor(MakeWritableByteSpan(*this), MakeByteSpan(key)); - } - /** Compute total memory usage of this object (own memory + any dynamic memory). */ size_t GetMemoryUsage() const noexcept; }; @@ -382,12 +371,12 @@ class AutoFile { protected: std::FILE* m_file; - std::vector m_obfuscation; + Obfuscation m_obfuscation; std::optional m_position; bool m_was_written{false}; public: - explicit AutoFile(std::FILE* file, std::vector obfuscation={}); + explicit AutoFile(std::FILE* file, const Obfuscation& obfuscation = {}); ~AutoFile() { @@ -435,7 +424,7 @@ public: bool IsNull() const { return m_file == nullptr; } /** Continue with a different XOR key */ - void SetObfuscation(std::vector obfuscation) { m_obfuscation = obfuscation; } + void SetObfuscation(const Obfuscation& obfuscation) { m_obfuscation = obfuscation; } /** Implementation detail, only used internally. */ std::size_t detail_fread(std::span dst); diff --git a/src/test/dbwrapper_tests.cpp b/src/test/dbwrapper_tests.cpp index 9c723351d6b..cd0f347b66e 100644 --- a/src/test/dbwrapper_tests.cpp +++ b/src/test/dbwrapper_tests.cpp @@ -9,21 +9,12 @@ #include #include +#include #include using util::ToString; -// Test if a string consists entirely of null characters -static bool is_null_key(const std::vector& key) { - bool isnull = true; - - for (unsigned int i = 0; i < key.size(); i++) - isnull &= (key[i] == '\x00'); - - return isnull; -} - BOOST_FIXTURE_TEST_SUITE(dbwrapper_tests, BasicTestingSetup) BOOST_AUTO_TEST_CASE(dbwrapper) @@ -33,7 +24,7 @@ BOOST_AUTO_TEST_CASE(dbwrapper) constexpr size_t CACHE_SIZE{1_MiB}; const fs::path path{m_args.GetDataDirBase() / "dbwrapper"}; - std::vector obfuscation_key{}; + Obfuscation obfuscation; std::vector> key_values{}; // Write values @@ -42,8 +33,8 @@ BOOST_AUTO_TEST_CASE(dbwrapper) BOOST_CHECK_EQUAL(obfuscate, !dbw.IsEmpty()); // Ensure that we're doing real obfuscation when obfuscate=true - obfuscation_key = dbwrapper_private::GetObfuscation(dbw); - BOOST_CHECK_EQUAL(obfuscate, !is_null_key(obfuscation_key)); + obfuscation = dbwrapper_private::GetObfuscation(dbw); + BOOST_CHECK_EQUAL(obfuscate, dbwrapper_private::GetObfuscation(dbw)); for (uint8_t k{0}; k < 10; ++k) { uint8_t key{k}; @@ -56,7 +47,7 @@ BOOST_AUTO_TEST_CASE(dbwrapper) // Verify that the obfuscation key is never obfuscated { CDBWrapper dbw{{.path = path, .cache_bytes = CACHE_SIZE, .obfuscate = false}}; - BOOST_CHECK(obfuscation_key == dbwrapper_private::GetObfuscation(dbw)); + BOOST_CHECK_EQUAL(obfuscation, dbwrapper_private::GetObfuscation(dbw)); } // Read back the values @@ -64,8 +55,8 @@ BOOST_AUTO_TEST_CASE(dbwrapper) CDBWrapper dbw{{.path = path, .cache_bytes = CACHE_SIZE, .obfuscate = obfuscate}}; // Ensure obfuscation is read back correctly - BOOST_CHECK(obfuscation_key == dbwrapper_private::GetObfuscation(dbw)); - BOOST_CHECK_EQUAL(obfuscate, !is_null_key(obfuscation_key)); + BOOST_CHECK_EQUAL(obfuscation, dbwrapper_private::GetObfuscation(dbw)); + BOOST_CHECK_EQUAL(obfuscate, dbwrapper_private::GetObfuscation(dbw)); // Verify all written values for (const auto& [key, expected_value] : key_values) { @@ -89,7 +80,7 @@ BOOST_AUTO_TEST_CASE(dbwrapper_basic_data) bool res_bool; // Ensure that we're doing real obfuscation when obfuscate=true - BOOST_CHECK_EQUAL(obfuscate, !is_null_key(dbwrapper_private::GetObfuscation(dbw))); + BOOST_CHECK_EQUAL(obfuscate, dbwrapper_private::GetObfuscation(dbw)); //Simulate block raw data - "b + block hash" std::string key_block = "b" + m_rng.rand256().ToString(); @@ -264,7 +255,7 @@ BOOST_AUTO_TEST_CASE(existing_data_no_obfuscate) BOOST_CHECK_EQUAL(res2.ToString(), in.ToString()); BOOST_CHECK(!odbw.IsEmpty()); - BOOST_CHECK(is_null_key(dbwrapper_private::GetObfuscation(odbw))); // The key should be an empty string + BOOST_CHECK(!dbwrapper_private::GetObfuscation(odbw)); // The key should be an empty string uint256 in2 = m_rng.rand256(); uint256 res3; @@ -301,7 +292,7 @@ BOOST_AUTO_TEST_CASE(existing_data_reindex) // Check that the key/val we wrote with unobfuscated wrapper doesn't exist uint256 res2; BOOST_CHECK(!odbw.Read(key, res2)); - BOOST_CHECK(!is_null_key(dbwrapper_private::GetObfuscation(odbw))); + BOOST_CHECK(dbwrapper_private::GetObfuscation(odbw)); uint256 in2 = m_rng.rand256(); uint256 res3; diff --git a/src/test/fuzz/autofile.cpp b/src/test/fuzz/autofile.cpp index 2cebba227f6..0de855668d0 100644 --- a/src/test/fuzz/autofile.cpp +++ b/src/test/fuzz/autofile.cpp @@ -22,7 +22,7 @@ FUZZ_TARGET(autofile) const auto key_bytes{ConsumeFixedLengthByteVector(fuzzed_data_provider, Obfuscation::KEY_SIZE)}; AutoFile auto_file{ fuzzed_file_provider.open(), - key_bytes, + Obfuscation{std::span{key_bytes}.first()}, }; LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 100) { diff --git a/src/test/fuzz/buffered_file.cpp b/src/test/fuzz/buffered_file.cpp index c61910e55a7..75ef8f313ae 100644 --- a/src/test/fuzz/buffered_file.cpp +++ b/src/test/fuzz/buffered_file.cpp @@ -24,7 +24,7 @@ FUZZ_TARGET(buffered_file) const auto key_bytes{ConsumeFixedLengthByteVector(fuzzed_data_provider, Obfuscation::KEY_SIZE)}; AutoFile fuzzed_file{ fuzzed_file_provider.open(), - key_bytes, + Obfuscation{std::span{key_bytes}.first()}, }; try { auto n_buf_size = fuzzed_data_provider.ConsumeIntegralInRange(0, 4096); diff --git a/src/test/streams_tests.cpp b/src/test/streams_tests.cpp index 346497b3c31..ce496df5a9f 100644 --- a/src/test/streams_tests.cpp +++ b/src/test/streams_tests.cpp @@ -21,10 +21,10 @@ BOOST_FIXTURE_TEST_SUITE(streams_tests, BasicTestingSetup) // Test that obfuscation can be properly reverted even with random chunk sizes. BOOST_AUTO_TEST_CASE(xor_roundtrip_random_chunks) { - auto apply_random_xor_chunks{[&](std::span target, std::span obfuscation) { + auto apply_random_xor_chunks{[&](std::span target, const Obfuscation& obfuscation) { for (size_t offset{0}; offset < target.size();) { const size_t chunk_size{1 + m_rng.randrange(target.size() - offset)}; - Obfuscation().Xor(target.subspan(offset, chunk_size), obfuscation, offset); + obfuscation(target.subspan(offset, chunk_size), offset); offset += chunk_size; } }}; @@ -35,13 +35,14 @@ BOOST_AUTO_TEST_CASE(xor_roundtrip_random_chunks) std::vector roundtrip{original}; const auto key_bytes{m_rng.randbool() ? m_rng.randbytes() : std::array{}}; - apply_random_xor_chunks(roundtrip, key_bytes); + const Obfuscation obfuscation{key_bytes}; + apply_random_xor_chunks(roundtrip, obfuscation); const bool key_all_zeros{std::ranges::all_of( std::span{key_bytes}.first(std::min(write_size, Obfuscation::KEY_SIZE)), [](auto b) { return b == std::byte{0}; })}; BOOST_CHECK(key_all_zeros ? original == roundtrip : original != roundtrip); - apply_random_xor_chunks(roundtrip, key_bytes); + apply_random_xor_chunks(roundtrip, obfuscation); BOOST_CHECK(original == roundtrip); } } @@ -62,24 +63,58 @@ BOOST_AUTO_TEST_CASE(xor_bytes_reference) const size_t write_offset{std::min(write_size, m_rng.randrange(Obfuscation::KEY_SIZE * 2))}; // Write unaligned data const auto key_bytes{m_rng.randbool() ? m_rng.randbytes() : std::array{}}; - const std::vector obfuscation{key_bytes.begin(), key_bytes.end()}; + const Obfuscation obfuscation{key_bytes}; std::vector expected{m_rng.randbytes(write_size)}; std::vector actual{expected}; expected_xor(std::span{expected}.subspan(write_offset), key_bytes, key_offset); - Obfuscation().Xor(std::span{actual}.subspan(write_offset), key_bytes, key_offset); + obfuscation(std::span{actual}.subspan(write_offset), key_offset); BOOST_CHECK_EQUAL_COLLECTIONS(expected.begin(), expected.end(), actual.begin(), actual.end()); } } +BOOST_AUTO_TEST_CASE(obfuscation_hexkey) +{ + const auto key_bytes{m_rng.randbytes()}; + + const Obfuscation obfuscation{key_bytes}; + BOOST_CHECK_EQUAL(obfuscation.HexKey(), HexStr(key_bytes)); +} + +BOOST_AUTO_TEST_CASE(obfuscation_serialize) +{ + const Obfuscation original{m_rng.randbytes()}; + + // Serialization + DataStream ds; + ds << original; + + BOOST_CHECK_EQUAL(ds.size(), 1 + Obfuscation::KEY_SIZE); // serialized as a vector + + // Deserialization + Obfuscation recovered{}; + ds >> recovered; + + BOOST_CHECK_EQUAL(recovered.HexKey(), original.HexKey()); +} + +BOOST_AUTO_TEST_CASE(obfuscation_empty) +{ + const Obfuscation null_obf{}; + BOOST_CHECK(!null_obf); + + const Obfuscation non_null_obf{"ff00ff00ff00ff00"_hex}; + BOOST_CHECK(non_null_obf); +} + BOOST_AUTO_TEST_CASE(xor_file) { fs::path xor_path{m_args.GetDataDirBase() / "test_xor.bin"}; auto raw_file{[&](const auto& mode) { return fsbridge::fopen(xor_path, mode); }}; const std::vector test1{1, 2, 3}; const std::vector test2{4, 5}; - const auto obfuscation{"ff00ff00ff00ff00"_hex_v}; + const Obfuscation obfuscation{"ff00ff00ff00ff00"_hex}; { // Check errors for missing file @@ -284,23 +319,23 @@ BOOST_AUTO_TEST_CASE(streams_serializedata_xor) // Degenerate case { DataStream ds{}; - ds.Xor("0000000000000000"_hex_v_u8); + Obfuscation{}(ds); BOOST_CHECK_EQUAL(""s, ds.str()); } { - const auto obfuscation{"ffffffffffffffff"_hex_v_u8}; + const Obfuscation obfuscation{"ffffffffffffffff"_hex}; DataStream ds{"0ff0"_hex}; - ds.Xor(obfuscation); + obfuscation(ds); BOOST_CHECK_EQUAL("\xf0\x0f"s, ds.str()); } { - const auto obfuscation{"ff0fff0fff0fff0f"_hex_v_u8}; + const Obfuscation obfuscation{"ff0fff0fff0fff0f"_hex}; DataStream ds{"f00f"_hex}; - ds.Xor(obfuscation); + obfuscation(ds); BOOST_CHECK_EQUAL("\x0f\x00"s, ds.str()); } } @@ -613,7 +648,7 @@ BOOST_AUTO_TEST_CASE(buffered_reader_matches_autofile_random_content) const FlatFilePos pos{0, 0}; const FlatFileSeq test_file{m_args.GetDataDirBase(), "buffered_file_test_random", node::BLOCKFILE_CHUNK_SIZE}; - const auto obfuscation{m_rng.randbytes(Obfuscation::KEY_SIZE)}; + const Obfuscation obfuscation{m_rng.randbytes()}; // Write out the file with random content { @@ -668,7 +703,7 @@ BOOST_AUTO_TEST_CASE(buffered_writer_matches_autofile_random_content) const FlatFileSeq test_buffered{m_args.GetDataDirBase(), "buffered_write_test", node::BLOCKFILE_CHUNK_SIZE}; const FlatFileSeq test_direct{m_args.GetDataDirBase(), "direct_write_test", node::BLOCKFILE_CHUNK_SIZE}; - const auto obfuscation{m_rng.randbytes(Obfuscation::KEY_SIZE)}; + const Obfuscation obfuscation{m_rng.randbytes()}; { DataBuffer test_data{m_rng.randbytes(file_size)}; diff --git a/src/util/obfuscation.h b/src/util/obfuscation.h index c39d7bf80a5..23bf805aa2a 100644 --- a/src/util/obfuscation.h +++ b/src/util/obfuscation.h @@ -7,19 +7,32 @@ #include #include +#include +#include + +#include class Obfuscation { public: - static constexpr size_t KEY_SIZE{sizeof(uint64_t)}; + using KeyType = uint64_t; + static constexpr size_t KEY_SIZE{sizeof(KeyType)}; - void Xor(std::span write, std::span key, size_t key_offset = 0) + Obfuscation() : m_key{KEY_SIZE, std::byte{0}} {} + explicit Obfuscation(std::span key_bytes) { - assert(key.size() == KEY_SIZE); + m_key = {key_bytes.begin(), key_bytes.end()}; + } + + operator bool() const { return ToKey() != 0; } + + void operator()(std::span write, size_t key_offset = 0) const + { + assert(m_key.size() == KEY_SIZE); key_offset %= KEY_SIZE; for (size_t i = 0, j = key_offset; i != write.size(); i++) { - write[i] ^= key[j++]; + write[i] ^= m_key[j++]; // This potentially acts on very many bytes of data, so it's // important that we calculate `j`, i.e. the `key` index in this @@ -29,6 +42,34 @@ public: j = 0; } } + + template + void Serialize(Stream& s) const + { + s << m_key; + } + + template + void Unserialize(Stream& s) + { + s >> m_key; + if (m_key.size() != KEY_SIZE) throw std::ios_base::failure(strprintf("Obfuscation key size should be exactly %s bytes long", KEY_SIZE)); + } + + std::string HexKey() const + { + return HexStr(m_key); + } + +private: + std::vector m_key; + + KeyType ToKey() const + { + KeyType key{}; + std::memcpy(&key, m_key.data(), KEY_SIZE); + return key; + } }; #endif // BITCOIN_UTIL_OBFUSCATION_H From e7114fc6dc3488c2584d42779ff2b102e4d1db99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Wed, 2 Jul 2025 11:24:21 +0200 Subject: [PATCH 11/12] optimization: migrate fixed-size obfuscation from `std::vector` to `uint64_t` All former `std::vector` keys were replaced with `uint64_t` (we still serialize them as vectors but convert immediately to `uint64_t` on load). This is why some tests still generate vector keys and convert them to `uint64_t` later instead of generating them directly. In `Obfuscation::Unserialize` we can safely throw an `std::ios_base::failure` since during mempool fuzzing `mempool_persist.cpp#L141` catches and ignored these errors. > C++ compiler .......................... GNU 14.2.0 | ns/byte | byte/s | err% | ins/byte | cyc/byte | IPC | bra/byte | miss% | total | benchmark |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:---------- | 0.04 | 28,365,698,819.44 | 0.0% | 0.34 | 0.13 | 2.714 | 0.07 | 0.0% | 5.33 | `ObfuscationBench` > C++ compiler .......................... Clang 20.1.7 | ns/byte | byte/s | err% | ins/byte | cyc/byte | IPC | bra/byte | miss% | total | benchmark |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:---------- | 0.08 | 13,012,464,203.00 | 0.0% | 0.65 | 0.28 | 2.338 | 0.13 | 0.8% | 5.50 | `ObfuscationBench` Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com> Co-authored-by: Ryan Ofsky --- src/util/obfuscation.h | 66 ++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/src/util/obfuscation.h b/src/util/obfuscation.h index 23bf805aa2a..b1170fa3363 100644 --- a/src/util/obfuscation.h +++ b/src/util/obfuscation.h @@ -10,6 +10,9 @@ #include #include +#include +#include +#include #include class Obfuscation @@ -18,58 +21,77 @@ public: using KeyType = uint64_t; static constexpr size_t KEY_SIZE{sizeof(KeyType)}; - Obfuscation() : m_key{KEY_SIZE, std::byte{0}} {} + Obfuscation() { SetRotations(0); } explicit Obfuscation(std::span key_bytes) { - m_key = {key_bytes.begin(), key_bytes.end()}; + SetRotations(ToKey(key_bytes)); } - operator bool() const { return ToKey() != 0; } + operator bool() const { return m_rotations[0] != 0; } - void operator()(std::span write, size_t key_offset = 0) const + void operator()(std::span target, size_t key_offset = 0) const { - assert(m_key.size() == KEY_SIZE); - key_offset %= KEY_SIZE; + if (!*this) return; - for (size_t i = 0, j = key_offset; i != write.size(); i++) { - write[i] ^= m_key[j++]; - - // This potentially acts on very many bytes of data, so it's - // important that we calculate `j`, i.e. the `key` index in this - // way instead of doing a %, which would effectively be a division - // for each byte Xor'd -- much slower than need be. - if (j == KEY_SIZE) - j = 0; + const KeyType rot_key{m_rotations[key_offset % KEY_SIZE]}; // Continue obfuscation from where we left off + for (; target.size() >= KEY_SIZE; target = target.subspan(KEY_SIZE)) { + XorWord(target.first(), rot_key); } + XorWord(target, rot_key); } template void Serialize(Stream& s) const { - s << m_key; + // Use vector serialization for convenient compact size prefix. + std::vector bytes{KEY_SIZE}; + std::memcpy(bytes.data(), &m_rotations[0], KEY_SIZE); + s << bytes; } template void Unserialize(Stream& s) { - s >> m_key; - if (m_key.size() != KEY_SIZE) throw std::ios_base::failure(strprintf("Obfuscation key size should be exactly %s bytes long", KEY_SIZE)); + std::vector bytes{KEY_SIZE}; + s >> bytes; + if (bytes.size() != KEY_SIZE) throw std::ios_base::failure(strprintf("Obfuscation key size should be exactly %s bytes long", KEY_SIZE)); + SetRotations(ToKey(std::span(bytes))); } std::string HexKey() const { - return HexStr(m_key); + return HexStr(std::bit_cast>(m_rotations[0])); } private: - std::vector m_key; + // Cached key rotations for different offsets. + std::array m_rotations; - KeyType ToKey() const + void SetRotations(KeyType key) + { + for (size_t i{0}; i < KEY_SIZE; ++i) { + int key_rotation_bits{int(CHAR_BIT * i)}; + if constexpr (std::endian::native == std::endian::big) key_rotation_bits *= -1; + m_rotations[i] = std::rotr(key, key_rotation_bits); + } + } + + static KeyType ToKey(std::span key_span) { KeyType key{}; - std::memcpy(&key, m_key.data(), KEY_SIZE); + std::memcpy(&key, key_span.data(), KEY_SIZE); return key; } + + static void XorWord(std::span target, KeyType key) + { + assert(target.size() <= KEY_SIZE); + if (target.empty()) return; + KeyType raw{}; + std::memcpy(&raw, target.data(), target.size()); + raw ^= key; + std::memcpy(target.data(), &raw, target.size()); + } }; #endif // BITCOIN_UTIL_OBFUSCATION_H From 248b6a27c351690d3596711cc36b8102977adeab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Thu, 22 May 2025 11:45:15 +0200 Subject: [PATCH 12/12] optimization: peel align-head and unroll body to 64 bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmarks indicated that obfuscating multiple bytes already gives an order of magnitude speed-up, but: * GCC still emitted scalar code; * Clang’s auto-vectorized loop ran on the slow unaligned-load path. Fix contains: * peeling the misaligned head enabled the hot loop starting at an 8-byte address; * `std::assume_aligned<8>` tells the optimizer the promise holds - required to keep Apple Clang happy; * manually unrolling the body to 64 bytes enabled GCC to auto-vectorize. Note that `target.size() > KEY_SIZE` condition is just an optimization, the aligned and unaligned loops work without it as well - it's why the alignment calculation still contains `std::min`. > C++ compiler .......................... GNU 14.2.0 | ns/byte | byte/s | err% | ins/byte | cyc/byte | IPC | bra/byte | miss% | total | benchmark |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:---------- | 0.03 | 32,464,658,919.11 | 0.0% | 0.50 | 0.11 | 4.474 | 0.08 | 0.0% | 5.29 | `ObfuscationBench` > C++ compiler .......................... Clang 20.1.7 | ns/byte | byte/s | err% | ins/byte | cyc/byte | IPC | bra/byte | miss% | total | benchmark |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:---------- | 0.02 | 41,231,547,045.17 | 0.0% | 0.30 | 0.09 | 3.463 | 0.02 | 0.0% | 5.47 | `ObfuscationBench` Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com> --- src/util/obfuscation.h | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/util/obfuscation.h b/src/util/obfuscation.h index b1170fa3363..db7527064b4 100644 --- a/src/util/obfuscation.h +++ b/src/util/obfuscation.h @@ -14,6 +14,7 @@ #include #include #include +#include class Obfuscation { @@ -33,9 +34,26 @@ public: { if (!*this) return; - const KeyType rot_key{m_rotations[key_offset % KEY_SIZE]}; // Continue obfuscation from where we left off - for (; target.size() >= KEY_SIZE; target = target.subspan(KEY_SIZE)) { - XorWord(target.first(), rot_key); + KeyType rot_key{m_rotations[key_offset % KEY_SIZE]}; // Continue obfuscation from where we left off + if (target.size() > KEY_SIZE) { + // Obfuscate until 64-bit alignment boundary + if (const auto misalign{std::bit_cast(target.data()) % KEY_SIZE}) { + const size_t alignment{std::min(KEY_SIZE - misalign, target.size())}; + XorWord(target.first(alignment), rot_key); + + target = {std::assume_aligned(target.data() + alignment), target.size() - alignment}; + rot_key = m_rotations[(key_offset + alignment) % KEY_SIZE]; + } + // Aligned obfuscation in 64-byte chunks + for (constexpr auto unroll{8}; target.size() >= KEY_SIZE * unroll; target = target.subspan(KEY_SIZE * unroll)) { + for (size_t i{0}; i < unroll; ++i) { + XorWord(target.subspan(i * KEY_SIZE, KEY_SIZE), rot_key); + } + } + // Aligned obfuscation in 64-bit chunks + for (; target.size() >= KEY_SIZE; target = target.subspan(KEY_SIZE)) { + XorWord(target.first(), rot_key); + } } XorWord(target, rot_key); }