From 8362318d25b9fdd2dbfa555af57915388fa3f681 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Wed, 10 Sep 2025 15:12:31 +0700 Subject: [PATCH] feat: add blake3 to conan/cmake --- Builds/CMake/RippledCore.cmake | 5 + CMakeLists.txt | 1 + conanfile.py | 1 + external/blake3/conandata.yml | 10 ++ external/blake3/conanfile.py | 102 +++++++++++++ src/test/protocol/blake3_test.cpp | 243 ++++++++++++++++++++++++++++++ 6 files changed, 362 insertions(+) create mode 100644 external/blake3/conandata.yml create mode 100644 external/blake3/conanfile.py create mode 100644 src/test/protocol/blake3_test.cpp diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index c9295a5f9..8d0d83374 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -153,6 +153,7 @@ target_link_libraries (xrpl_core Ripple::syslibs secp256k1::secp256k1 ed25519::ed25519 + BLAKE3::blake3 date::date Ripple::opts) #[=================================[ @@ -974,6 +975,7 @@ if (tests) test sources: subdir: protocol #]===============================] + src/test/protocol/blake3_test.cpp src/test/protocol/BuildInfo_test.cpp src/test/protocol/InnerObjectFormats_test.cpp src/test/protocol/Issue_test.cpp @@ -1069,6 +1071,9 @@ target_link_libraries (rippled Ripple::opts Ripple::libs Ripple::xrpl_core + BLAKE3::blake3 + # Workaround for a Conan 1.x bug... + m ) exclude_if_included (rippled) # define a macro for tests that might need to diff --git a/CMakeLists.txt b/CMakeLists.txt index 57fc86e2c..6f10d5183 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,6 +129,7 @@ else() endif() find_package(nudb REQUIRED) find_package(date REQUIRED) + find_package(BLAKE3 REQUIRED) include(conan/Protobuf) include(conan/gRPC) if(TARGET nudb::core) diff --git a/conanfile.py b/conanfile.py index 9dea082ff..2d33230cf 100644 --- a/conanfile.py +++ b/conanfile.py @@ -24,6 +24,7 @@ class Xrpl(ConanFile): } requires = [ + 'blake3/1.5.0@xahaud/stable', 'boost/1.86.0', 'date/3.0.1', 'libarchive/3.6.0', diff --git a/external/blake3/conandata.yml b/external/blake3/conandata.yml new file mode 100644 index 000000000..b8849f52e --- /dev/null +++ b/external/blake3/conandata.yml @@ -0,0 +1,10 @@ +sources: + "1.5.0": + url: "https://github.com/BLAKE3-team/BLAKE3/archive/refs/tags/1.5.0.tar.gz" + sha256: "f506140bc3af41d3432a4ce18b3b83b08eaa240e94ef161eb72b2e57cdc94c69" + "1.4.1": + url: "https://github.com/BLAKE3-team/BLAKE3/archive/refs/tags/1.4.1.tar.gz" + sha256: "33020ac83a8169b2e847cc6fb1dd38806ffab6efe79fe6c320e322154a3bea2c" + "1.4.0": + url: "https://github.com/BLAKE3-team/BLAKE3/archive/refs/tags/1.4.0.tar.gz" + sha256: "658e1c75e2d9bbed9f426385f02d2a188dc19978a39e067ba93e837861e5fe58" \ No newline at end of file diff --git a/external/blake3/conanfile.py b/external/blake3/conanfile.py new file mode 100644 index 000000000..bc1b365a2 --- /dev/null +++ b/external/blake3/conanfile.py @@ -0,0 +1,102 @@ +from conan import ConanFile +from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout +from conan.tools.files import copy, get +from conan.tools.scm import Version +import os + +required_conan_version = ">=1.54.0" + + +class Blake3Conan(ConanFile): + name = "blake3" + version = "1.5.0" + description = "BLAKE3 cryptographic hash function" + topics = ("blake3", "hash", "cryptography") + url = "https://github.com/BLAKE3-team/BLAKE3" + homepage = "https://github.com/BLAKE3-team/BLAKE3" + license = "CC0-1.0 OR Apache-2.0" + + package_type = "library" + settings = "os", "arch", "compiler", "build_type" + options = { + "shared": [True, False], + "fPIC": [True, False], + "simd": [True, False], + } + default_options = { + "shared": False, + "fPIC": True, + "simd": True, + } + + def config_options(self): + if self.settings.os == 'Windows': + del self.options.fPIC + + def configure(self): + if self.options.shared: + self.options.rm_safe("fPIC") + # BLAKE3 is C code + self.settings.rm_safe("compiler.cppstd") + self.settings.rm_safe("compiler.libcxx") + + def layout(self): + cmake_layout(self, src_folder="src") + + def source(self): + get(self, **self.conan_data["sources"][self.version], strip_root=True) + + def generate(self): + tc = CMakeToolchain(self) + # BLAKE3's CMake options + tc.variables["BUILD_SHARED_LIBS"] = self.options.shared + if not self.options.simd: + tc.variables["BLAKE3_NO_SSE2"] = True + tc.variables["BLAKE3_NO_SSE41"] = True + tc.variables["BLAKE3_NO_AVX2"] = True + tc.variables["BLAKE3_NO_AVX512"] = True + tc.variables["BLAKE3_NO_NEON"] = True + tc.generate() + + def build(self): + cmake = CMake(self) + # BLAKE3's C implementation has its CMakeLists.txt in the c/ subdirectory + cmake.configure(build_script_folder=os.path.join(self.source_folder, "c")) + cmake.build() + + def package(self): + # Copy license files + copy(self, "LICENSE*", src=self.source_folder, + dst=os.path.join(self.package_folder, "licenses")) + # Copy header + copy(self, "blake3.h", + src=os.path.join(self.source_folder, "c"), + dst=os.path.join(self.package_folder, "include")) + # Copy library + copy(self, "*.a", src=self.build_folder, + dst=os.path.join(self.package_folder, "lib"), keep_path=False) + copy(self, "*.lib", src=self.build_folder, + dst=os.path.join(self.package_folder, "lib"), keep_path=False) + copy(self, "*.dylib", src=self.build_folder, + dst=os.path.join(self.package_folder, "lib"), keep_path=False) + copy(self, "*.so*", src=self.build_folder, + dst=os.path.join(self.package_folder, "lib"), keep_path=False) + copy(self, "*.dll", src=self.build_folder, + dst=os.path.join(self.package_folder, "bin"), keep_path=False) + + def package_info(self): + self.cpp_info.set_property("cmake_file_name", "BLAKE3") + self.cpp_info.set_property("cmake_target_name", "BLAKE3::blake3") + + # IMPORTANT: Explicitly set include directories to fix Conan CMakeDeps generation + self.cpp_info.includedirs = ["include"] + self.cpp_info.libs = ["blake3"] + + # System libraries + if self.settings.os in ["Linux", "FreeBSD"]: + self.cpp_info.system_libs.append("m") + self.cpp_info.system_libs.append("pthread") + + # TODO: to remove in conan v2 once cmake_find_package* generators removed + self.cpp_info.names["cmake_find_package"] = "BLAKE3" + self.cpp_info.names["cmake_find_package_multi"] = "BLAKE3" \ No newline at end of file diff --git a/src/test/protocol/blake3_test.cpp b/src/test/protocol/blake3_test.cpp new file mode 100644 index 000000000..2658c6c2b --- /dev/null +++ b/src/test/protocol/blake3_test.cpp @@ -0,0 +1,243 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include + +namespace ripple { + +class blake3_test : public beast::unit_test::suite +{ +public: + void + testBasicHashing() + { + testcase("Basic BLAKE3 hashing"); + + // Test vector from BLAKE3 specification + const std::string input = "abc"; + std::array output; + + blake3_hasher hasher; + blake3_hasher_init(&hasher); + blake3_hasher_update(&hasher, input.data(), input.size()); + blake3_hasher_finalize(&hasher, output.data(), BLAKE3_OUT_LEN); + + // Expected hash for "abc" (verified with online BLAKE3 calculator) + const std::array expected = { + 0x64, 0x37, 0xb3, 0xac, 0x38, 0x46, 0x51, 0x33, + 0xff, 0xb6, 0x3b, 0x75, 0x27, 0x3a, 0x8d, 0xb5, + 0x48, 0xc5, 0x58, 0x46, 0x5d, 0x79, 0xdb, 0x03, + 0xfd, 0x35, 0x9c, 0x6c, 0xd5, 0xbd, 0x9d, 0x85 + }; + + BEAST_EXPECT(output == expected); + } + + void + testEmptyInput() + { + testcase("BLAKE3 empty input"); + + std::array output; + + blake3_hasher hasher; + blake3_hasher_init(&hasher); + blake3_hasher_finalize(&hasher, output.data(), BLAKE3_OUT_LEN); + + // Expected hash for empty input + const std::array expected = { + 0xaf, 0x13, 0x49, 0xb9, 0xf5, 0xf9, 0xa1, 0xa6, + 0xa0, 0x40, 0x4d, 0xea, 0x36, 0xdc, 0xc9, 0x49, + 0x9b, 0xcb, 0x25, 0xc9, 0xad, 0xc1, 0x12, 0xb7, + 0xcc, 0x9a, 0x93, 0xca, 0xe4, 0x1f, 0x32, 0x62 + }; + + BEAST_EXPECT(output == expected); + } + + void + testIncrementalHashing() + { + testcase("BLAKE3 incremental hashing"); + + // Hash "Hello, World!" in chunks + const std::string part1 = "Hello, "; + const std::string part2 = "World!"; + std::array incremental_output; + + blake3_hasher hasher; + blake3_hasher_init(&hasher); + blake3_hasher_update(&hasher, part1.data(), part1.size()); + blake3_hasher_update(&hasher, part2.data(), part2.size()); + blake3_hasher_finalize(&hasher, incremental_output.data(), BLAKE3_OUT_LEN); + + // Hash the same string all at once + const std::string full = part1 + part2; + std::array single_output; + + blake3_hasher hasher2; + blake3_hasher_init(&hasher2); + blake3_hasher_update(&hasher2, full.data(), full.size()); + blake3_hasher_finalize(&hasher2, single_output.data(), BLAKE3_OUT_LEN); + + // Both methods should produce the same hash + BEAST_EXPECT(incremental_output == single_output); + } + + void + testLargeInput() + { + testcase("BLAKE3 large input"); + + // Create a large input (10KB) + std::vector large_input(10240); + for (size_t i = 0; i < large_input.size(); ++i) + { + large_input[i] = static_cast(i & 0xFF); + } + + std::array output; + + blake3_hasher hasher; + blake3_hasher_init(&hasher); + blake3_hasher_update(&hasher, large_input.data(), large_input.size()); + blake3_hasher_finalize(&hasher, output.data(), BLAKE3_OUT_LEN); + + // Just verify it doesn't crash and produces a hash + // Check that output is not all zeros + bool not_all_zeros = false; + for (auto byte : output) + { + if (byte != 0) + { + not_all_zeros = true; + break; + } + } + BEAST_EXPECT(not_all_zeros); + } + + void + testVariableOutputLength() + { + testcase("BLAKE3 variable output length"); + + const std::string input = "test"; + + // Test different output lengths + std::array output16; + std::array output32; + std::array output64; + + blake3_hasher hasher1; + blake3_hasher_init(&hasher1); + blake3_hasher_update(&hasher1, input.data(), input.size()); + blake3_hasher_finalize(&hasher1, output16.data(), 16); + + blake3_hasher hasher2; + blake3_hasher_init(&hasher2); + blake3_hasher_update(&hasher2, input.data(), input.size()); + blake3_hasher_finalize(&hasher2, output32.data(), 32); + + blake3_hasher hasher3; + blake3_hasher_init(&hasher3); + blake3_hasher_update(&hasher3, input.data(), input.size()); + blake3_hasher_finalize(&hasher3, output64.data(), 64); + + // First 16 bytes of 32-byte output should match 16-byte output + BEAST_EXPECT(std::memcmp(output16.data(), output32.data(), 16) == 0); + + // First 32 bytes of 64-byte output should match 32-byte output + BEAST_EXPECT(std::memcmp(output32.data(), output64.data(), 32) == 0); + } + + void + testKeyedMode() + { + testcase("BLAKE3 keyed mode"); + + // 32-byte key + uint8_t key[BLAKE3_KEY_LEN]; + std::memset(key, 0x42, BLAKE3_KEY_LEN); // Fill with 0x42 + + const std::string input = "message"; + std::array keyed_output; + + blake3_hasher hasher; + blake3_hasher_init_keyed(&hasher, key); + blake3_hasher_update(&hasher, input.data(), input.size()); + blake3_hasher_finalize(&hasher, keyed_output.data(), BLAKE3_OUT_LEN); + + // Hash without key should produce different output + std::array unkeyed_output; + blake3_hasher hasher2; + blake3_hasher_init(&hasher2); + blake3_hasher_update(&hasher2, input.data(), input.size()); + blake3_hasher_finalize(&hasher2, unkeyed_output.data(), BLAKE3_OUT_LEN); + + BEAST_EXPECT(keyed_output != unkeyed_output); + } + + void + testDerivationMode() + { + testcase("BLAKE3 key derivation mode"); + + const char* context = "ripple 2024 key derivation"; + const std::string input = "input key material"; + std::array derived_key; + + blake3_hasher hasher; + blake3_hasher_init_derive_key(&hasher, context); + blake3_hasher_update(&hasher, input.data(), input.size()); + blake3_hasher_finalize(&hasher, derived_key.data(), BLAKE3_OUT_LEN); + + // Different context should produce different output + const char* context2 = "different context"; + std::array derived_key2; + + blake3_hasher hasher2; + blake3_hasher_init_derive_key(&hasher2, context2); + blake3_hasher_update(&hasher2, input.data(), input.size()); + blake3_hasher_finalize(&hasher2, derived_key2.data(), BLAKE3_OUT_LEN); + + BEAST_EXPECT(derived_key != derived_key2); + } + + void + run() override + { + testBasicHashing(); + testEmptyInput(); + testIncrementalHashing(); + testLargeInput(); + testVariableOutputLength(); + testKeyedMode(); + testDerivationMode(); + } +}; + +BEAST_DEFINE_TESTSUITE(blake3, protocol, ripple); + +} // namespace ripple \ No newline at end of file