From d963785d874f28c46a3c3890649da0257da57339 Mon Sep 17 00:00:00 2001 From: William Allen Date: Fri, 4 Aug 2023 21:17:15 -0500 Subject: [PATCH] Adding compressed harvesting (#370) Signed-off-by: wallentx Co-authored-by: Author: Florin Chirica Co-authored-by: Harold Brenes Co-authored-by: Amine Khaldi Co-authored-by: Kyle Altendorf Co-authored-by: Chris Marslender Co-authored-by: arvidn Co-authored-by: Phill Walker <72089507+PhillWalker@users.noreply.github.com> --- .github/actions/fetch_bladebit_harvester.sh | 93 ++++ .github/workflows/build-wheels.yml | 87 ++- .gitignore | 6 + CMakeLists.txt | 109 +++- python-bindings/chiapos.cpp | 5 + setup.py | 17 + src/cli.cpp | 34 +- src/prover_disk.hpp | 561 +++++++++++++++++++- 8 files changed, 820 insertions(+), 92 deletions(-) create mode 100755 .github/actions/fetch_bladebit_harvester.sh diff --git a/.github/actions/fetch_bladebit_harvester.sh b/.github/actions/fetch_bladebit_harvester.sh new file mode 100755 index 000000000..a757941b0 --- /dev/null +++ b/.github/actions/fetch_bladebit_harvester.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -eo pipefail +_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +cd "$_dir/../.." + +## +# Usage: fetch_bladebit_harvester.sh +# +# Use gitbash or similar under Windows. +## +host_os=$1 +host_arch=$2 + +if [[ "${host_os}" != "linux" ]] && [[ "${host_os}" != "macos" ]] && [[ "${host_os}" != "windows" ]]; then + echo >&2 "Unkonwn OS '${host_os}'" + exit 1 +fi + +if [[ "${host_arch}" != "arm64" ]] && [[ "${host_arch}" != "x86-64" ]]; then + echo >&2 "Unkonwn Architecture '${host_arch}'" + exit 1 +fi + +## Change this if including a new bladebit release +artifact_ver="v3.0.0" +artifact_base_url="https://github.com/Chia-Network/bladebit/releases/download/v3.0.0" + +linux_arm_sha256="5a53d82c2cc22172bfa2267ea9fb53126a527eba7bafd03ddf5503913a61f70c" +linux_x86_sha256="3cdbcf127126d7c61f6da715b25ef73a8420778dd34d56e82ed1865d7a1ebfeb" +macos_arm_sha256="325150951e83be4ee8690be996e6fde0776ff4cca89e39111c97f0aae3f93bf3" +macos_x86_sha256="718ab50a19ea3a8f064f2d09df38720cbb7d89667b599f57f2bca3cdf41c18e9" +windows_sha256="f3e14d02daafaa8e3666ab4666220d3b2859b1c10254bddb38a69da83cc899c5" +## End changes + +artifact_ext="tar.gz" +sha_bin="sha256sum" +expected_sha256= + +if [[ "$OSTYPE" == "darwin"* ]]; then + sha_bin="shasum -a 256" +fi + +case "${host_os}" in +linux) + if [[ "${host_arch}" == "arm64" ]]; then + expected_sha256=$linux_arm_sha256 + else + expected_sha256=$linux_x86_sha256 + fi + ;; +macos) + if [[ "${host_arch}" == "arm64" ]]; then + expected_sha256=$macos_arm_sha256 + else + expected_sha256=$macos_x86_sha256 + fi + ;; +windows) + expected_sha256=$windows_sha256 + artifact_ext="zip" + ;; +*) + echo >&2 "Unexpected OS '${host_os}'" + exit 1 + ;; +esac + +# Download artifact +artifact_name="green_reaper.${artifact_ext}" +curl -L "${artifact_base_url}/green_reaper-${artifact_ver}-${host_os}-${host_arch}.${artifact_ext}" >"${artifact_name}" + +# Validate sha256, if one was given +if [ -n "${expected_sha256}" ]; then + gr_sha256="$(${sha_bin} ${artifact_name} | cut -d' ' -f1)" + + if [[ "${gr_sha256}" != "${expected_sha256}" ]]; then + echo >&2 "GreenReaper SHA256 mismatch!" + echo >&2 " Got : '${gr_sha256}'" + echo >&2 " Expected: '${expected_sha256}'" + exit 1 + fi +fi + +# Unpack artifact +dst_dir="libs/green_reaper" +mkdir -p "${dst_dir}" +if [[ "${artifact_ext}" == "zip" ]]; then + unzip -d "${dst_dir}" "${artifact_name}" +else + pushd "${dst_dir}" + tar -xzvf "../../${artifact_name}" + popd +fi diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 04b523677..2694aa89b 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -4,8 +4,8 @@ on: push: branches: - main - tags: - - '**' + release: + types: [published] pull_request: branches: - '**' @@ -19,6 +19,7 @@ jobs: build-wheels: name: Wheel - ${{ matrix.os.name }} ${{ matrix.python.major-dot-minor }} ${{ matrix.arch.name }} runs-on: ${{ matrix.os.runs-on[matrix.arch.matrix] }} + continue-on-error: true strategy: fail-fast: false matrix: @@ -41,29 +42,23 @@ jobs: runs-on: intel: [windows-latest] python: - - major-dot-minor: '3.7' - cibw-build: 'cp37-*' - manylinux: - arm: manylinux2014 - intel: manylinux2010 - matrix: '3.7' - major-dot-minor: '3.8' cibw-build: 'cp38-*' manylinux: arm: manylinux2014 - intel: manylinux2010 + intel: manylinux2014 matrix: '3.8' - major-dot-minor: '3.9' cibw-build: 'cp39-*' manylinux: arm: manylinux2014 - intel: manylinux2010 + intel: manylinux2014 matrix: '3.9' - major-dot-minor: '3.10' cibw-build: 'cp310-*' manylinux: arm: manylinux2014 - intel: manylinux2010 + intel: manylinux2014 matrix: '3.10' - major-dot-minor: '3.11' cibw-build: 'cp311-*' @@ -96,9 +91,9 @@ jobs: arm: [macOS, ARM64] intel: [macos-latest] python: - major-dot-minor: '3.7' - cibw-build: 'cp37-*' - matrix: '3.7' + major-dot-minor: '3.8' + cibw-build: 'cp38-*' + matrix: '3.8' arch: name: ARM matrix: arm @@ -112,6 +107,12 @@ jobs: with: fetch-depth: 0 + - name: Set Env + if: env.RUNNER_ARCH != 'ARM64' + uses: Chia-Network/actions/setjobenv@main + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: Chia-Network/actions/setup-python@main with: python-version: ${{ matrix.python.major-dot-minor }} @@ -120,32 +121,55 @@ jobs: run: | pip install pipx + - name: Get Windows Bladebit Harvester Artifact + if: runner.os == 'Windows' + shell: bash + run: | + set -eo pipefail + set -x + .github/actions/fetch_bladebit_harvester.sh windows x86-64 + - name: Build and test env: CIBW_PRERELEASE_PYTHONS: True - CIBW_BUILD_VERBOSITY_MACOS: 0 - CIBW_BUILD_VERBOSITY_LINUX: 0 - CIBW_BUILD_VERBOSITY_WINDOWS: 0 + CIBW_BUILD_VERBOSITY_MACOS: 1 + CIBW_BUILD_VERBOSITY_LINUX: 1 + CIBW_BUILD_VERBOSITY_WINDOWS: 1 CIBW_BUILD: ${{ matrix.python.cibw-build }} CIBW_SKIP: '*-manylinux_i686 *-win32 *-musllinux_*' CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.python.manylinux['arm'] }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.python.manylinux['intel'] }} - CIBW_ENVIRONMENT_LINUX: "PATH=/project/cmake-3.17.3-Linux-`uname -m`/bin:$PATH" - CIBW_BEFORE_ALL_LINUX: > - curl -L https://github.com/Kitware/CMake/releases/download/v3.17.3/cmake-3.17.3-Linux-`uname -m`.sh > cmake.sh - && yes | sh cmake.sh | cat - && rm -f /usr/bin/cmake - && which cmake - && cmake --version - && uname -a + CIBW_ENVIRONMENT_WINDOWS: "CP_USE_GREEN_REAPER=1" + CIBW_ENVIRONMENT_LINUX: CP_USE_GREEN_REAPER="1" + CIBW_BEFORE_ALL_LINUX: | + set -eo pipefail + set -x + # Get bladebit harvester + set -eo pipefail + ARCH=$(uname -m) + if [[ $ARCH == x86_64 ]]; then + .github/actions/fetch_bladebit_harvester.sh linux x86-64 + else + .github/actions/fetch_bladebit_harvester.sh linux arm64 + fi + CIBW_BEFORE_BUILD_LINUX: > python -m pip install --upgrade pip CIBW_ARCHS_MACOS: ${{ matrix.os.cibw-archs-macos[matrix.arch.matrix] }} - CIBW_BEFORE_ALL_MACOS: > + CIBW_BEFORE_ALL_MACOS: | brew install gmp boost cmake + # Get bladebit harvester + set -eo pipefail + ARCH=$(uname -m) + if [[ $ARCH == x86_64 ]]; then + .github/actions/fetch_bladebit_harvester.sh macos x86-64 + else + .github/actions/fetch_bladebit_harvester.sh macos arm64 + fi + CIBW_BEFORE_BUILD_MACOS: > python -m pip install --upgrade pip - CIBW_ENVIRONMENT_MACOS: "MACOSX_DEPLOYMENT_TARGET=10.14" + CIBW_ENVIRONMENT_MACOS: "MACOSX_DEPLOYMENT_TARGET=10.14 CP_USE_GREEN_REAPER=1" CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: py.test -v {project}/tests run: @@ -274,6 +298,11 @@ jobs: with: fetch-depth: 0 + - name: Set Env + uses: Chia-Network/actions/setjobenv@main + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: Chia-Network/actions/setup-python@main with: python-version: ${{ matrix.python.major-dot-minor }} @@ -298,7 +327,7 @@ jobs: run: pip install twine - name: Publish distribution to PyPI - if: startsWith(github.event.ref, 'refs/tags') && steps.check_secrets.outputs.HAS_SECRET + if: env.RELEASE == 'true' && steps.check_secrets.outputs.HAS_SECRET env: TWINE_USERNAME: __token__ TWINE_NON_INTERACTIVE: 1 @@ -306,7 +335,7 @@ jobs: run: twine upload --non-interactive --skip-existing --verbose 'dist/*' - name: Publish distribution to Test PyPI - if: steps.check_secrets.outputs.HAS_SECRET + if: env.PRE_RELEASE == 'true' && steps.check_secrets.outputs.HAS_SECRET env: TWINE_REPOSITORY_URL: https://test.pypi.org/legacy/ TWINE_USERNAME: __token__ diff --git a/.gitignore b/.gitignore index 8ba5e60ea..698734681 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,9 @@ build .mypy_cache *.whl venv +build-tsan +build-* +cmake-build* +*.zip +*.tar.gz +libs/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e653b42c..207aacc8e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,26 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(cxxopts) + +option(CP_LINK_BLADEBIT_HARVESTER "Links libbladebit_harvester at build time instead of dynamically loading it." OFF) +option(CP_BUILD_BLADEBIT_HARVESTER "Pulls bladebit harvester target from git and builds it as a dependency.") + +if (${CP_BUILD_BLADEBIT_HARVESTER} AND NOT ${CP_LINK_BLADEBIT_HARVESTER}) + set(CP_LINK_BLADEBIT_HARVESTER ON) +endif() + +if (${CP_BUILD_BLADEBIT_HARVESTER}) + FetchContent_Declare( + bladebit + GIT_REPOSITORY https://github.com/Chia-Network/bladebit.git + GIT_TAG cuda-compression + ) + + set(BB_HARVESTER_ONLY ON) + set(BB_HARVESTER_STATIC ON) + FetchContent_MakeAvailable(bladebit) +endif() + FetchContent_Declare( gulrak GIT_REPOSITORY https://github.com/gulrak/filesystem.git @@ -57,7 +77,11 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/../lib/FiniteStateEntropy/lib ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/test - ) +) + +IF (${CP_LINK_BLADEBIT_HARVESTER}) + message ("Bladebit Harvesting Enabled") +ENDIF () add_library(fse ${FSE_FILES}) @@ -161,13 +185,6 @@ add_executable(RunTests ${BLAKE3_SRC} ) -target_link_libraries(RunTests - PRIVATE - fse - Threads::Threads - Catch2::Catch2 -) - find_package(Threads REQUIRED) add_library(uint128 STATIC uint128_t/uint128_t.cpp) @@ -175,26 +192,60 @@ target_include_directories(uint128 PUBLIC uint128_t) target_compile_features(fse PUBLIC cxx_std_17) target_compile_features(chiapos PUBLIC cxx_std_17) -target_compile_features(RunTests PUBLIC cxx_std_17) - -if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - target_link_libraries(chiapos PRIVATE fse Threads::Threads) - target_link_libraries(ProofOfSpace fse Threads::Threads) -elseif (${CMAKE_SYSTEM_NAME} MATCHES "OpenBSD") - target_link_libraries(chiapos PRIVATE fse Threads::Threads) - target_link_libraries(ProofOfSpace fse Threads::Threads) -elseif (${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD") - target_link_libraries(chiapos PRIVATE fse Threads::Threads) - target_link_libraries(ProofOfSpace fse Threads::Threads) -elseif (MSVC) - target_link_libraries(chiapos PRIVATE fse Threads::Threads uint128) - target_link_libraries(ProofOfSpace fse Threads::Threads uint128) - target_link_libraries(RunTests PRIVATE uint128) -else() - target_link_libraries(chiapos PRIVATE fse stdc++fs Threads::Threads) - target_link_libraries(ProofOfSpace fse stdc++fs Threads::Threads) - target_link_libraries(RunTests PRIVATE stdc++fs) +# target_compile_features(RunTests PUBLIC cxx_std_17) + +target_link_libraries(chiapos PRIVATE fse Threads::Threads + $<$:uint128> + $<$>:stdc++fs> +) +target_link_libraries(ProofOfSpace PRIVATE fse Threads::Threads + $<$:uint128> + $<$>:stdc++fs> +) +target_link_libraries(RunTests PRIVATE fse Threads::Threads Catch2::Catch2WithMain + $<$:uint128> + $<$>:stdc++fs> +) + +if (${CP_LINK_BLADEBIT_HARVESTER}) + + set(bb_defs + USE_GREEN_REAPER=1 + BLADEBIT_HARVESTER_LINKED=1 + $<$:BLADEBIT_IS_PROJECT_DEPENDENCY=1> + ) + set(bb_libs + bladebit_harvester + $<$>:dl> + ) + + include_directories( + ${INCLUDE_DIRECTORIES} + ${CMAKE_CURRENT_SOURCE_DIR}/libs/green_reaper/include + ) + + link_directories( + ${LINK_DIRECTORIES} + ${CMAKE_SOURCE_DIR}/libs/green_reaper/lib + ) + + target_compile_definitions(chiapos PUBLIC ${bb_defs}) + target_compile_definitions(ProofOfSpace PUBLIC ${bb_defs}) + target_compile_definitions(RunTests PUBLIC ${bb_defs}) + + target_link_libraries(chiapos PUBLIC ${bb_libs}) + target_link_libraries(ProofOfSpace PUBLIC ${bb_libs}) + target_link_libraries(RunTests PUBLIC ${bb_libs}) + + target_link_directories(chiapos PUBLIC ${CMAKE_SOURCE_DIR}/libs/green_reaper/lib) + target_link_directories(ProofOfSpace PUBLIC ${CMAKE_SOURCE_DIR}/libs/green_reaper/lib) + target_link_directories(RunTests PUBLIC ${CMAKE_SOURCE_DIR}/libs/green_reaper/lib) + + set_property(TARGET chiapos APPEND PROPERTY BUILD_RPATH "$ORIGIN") + set_property(TARGET ProofOfSpace APPEND PROPERTY BUILD_RPATH "$ORIGIN") + set_property(TARGET RunTests APPEND PROPERTY BUILD_RPATH "$ORIGIN") endif() -enable_testing() -add_test(NAME RunTests COMMAND RunTests) + +#enable_testing() +#add_test(NAME RunTests COMMAND RunTests) diff --git a/python-bindings/chiapos.cpp b/python-bindings/chiapos.cpp index 42431aa54..3a1a95a21 100644 --- a/python-bindings/chiapos.cpp +++ b/python-bindings/chiapos.cpp @@ -105,6 +105,7 @@ PYBIND11_MODULE(chiapos, m) return py::bytes(reinterpret_cast(id.data()), id.size()); }) .def("get_size", [](DiskProver &dp) { return dp.GetSize(); }) + .def("get_compression_level", [](DiskProver &dp) { return dp.GetCompressionLevel(); }) .def("get_filename", [](DiskProver &dp) { return dp.GetFilename(); }) .def( "get_qualities_for_challenge", @@ -179,6 +180,10 @@ PYBIND11_MODULE(chiapos, m) delete[] quality_buf; return stdx::optional(quality_py); }); + + py::class_(m, "ContextQueue") + .def("init", &ContextQueue::init); + m.attr("decompressor_context_queue") = &decompressor_context_queue; } #endif // PYTHON_BINDINGS_PYTHON_BINDINGS_HPP_ diff --git a/setup.py b/setup.py index 44d097dff..244e2ae56 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import os import re +import shutil import sys import platform import subprocess @@ -44,6 +45,9 @@ def build_extension(self, ext): "-DPYTHON_EXECUTABLE=" + sys.executable, ] + if os.getenv("CP_USE_GREEN_REAPER") == "1": + cmake_args.append("-DCP_LINK_BLADEBIT_HARVESTER=ON") + cfg = "Debug" if self.debug else "Release" build_args = ["--config", cfg] @@ -177,11 +181,24 @@ def build_extensions(self): opts.append("-fvisibility=hidden") elif ct == "msvc": opts.append('/DVERSION_INFO=\\"%s\\"' % self.distribution.get_version()) + + # Link bladebit_harvester + if os.getenv("CP_USE_GREEN_REAPER") == "1": + opts.append("/DUSE_GREEN_REAPER=1") + opts.append("/DBLADEBIT_HARVESTER_LINKED=1") + opts.append("/Ilibs/green_reaper/include") + link_opts.append("libs/green_reaper/lib/bladebit_harvester.lib") + for ext in self.extensions: ext.extra_compile_args = opts ext.extra_link_args = link_opts build_ext.build_extensions(self) + # Copy bladebit_harvester.dll on windows to the target build directory + # in order to package it into the root directory of the wheel + if os.getenv("CP_USE_GREEN_REAPER") == "1" and sys.platform == "win32": + shutil.copy2("libs/green_reaper/lib/bladebit_harvester.dll", self.build_lib + "/bladebit_harvester.dll") + if platform.system() == "Windows": setup( diff --git a/src/cli.cpp b/src/cli.cpp index 6156dce4a..526a72140 100644 --- a/src/cli.cpp +++ b/src/cli.cpp @@ -63,6 +63,17 @@ void HelpAndQuit(cxxopts::Options options) exit(0); } +// Not thread safe +inline void InitDecompressorQueueDefault(bool no_cuda = false) +{ + static bool initialized = false; + if (initialized) { + return; + } + decompressor_context_queue.init(1, (uint32_t)std::thread::hardware_concurrency(), false, 9, !no_cuda, 0, false); + initialized = true; +} + int main(int argc, char *argv[]) try { cxxopts::Options options( "ProofOfSpace", "Utility for plotting, generating and verifying proofs of space."); @@ -161,6 +172,8 @@ int main(int argc, char *argv[]) try { num_threads, phases_flags); } else if (operation == "prove") { + InitDecompressorQueueDefault(); + if (argc < 3) { HelpAndQuit(options); } @@ -238,6 +251,8 @@ int main(int argc, char *argv[]) try { } delete[] proof_bytes; } else if (operation == "check") { + InitDecompressorQueueDefault(); + uint32_t iterations = 1000; if (argc == 3) { iterations = std::stoi(argv[2]); @@ -247,6 +262,8 @@ int main(int argc, char *argv[]) try { Verifier verifier = Verifier(); uint32_t success = 0; + uint32_t failures = 0; + uint32_t exceptions = 0; std::vector id_bytes = prover.GetId(); k = prover.GetSize(); @@ -257,10 +274,10 @@ int main(int argc, char *argv[]) try { vector hash(picosha2::k_digest_size); picosha2::hash256(hash_input.begin(), hash_input.end(), hash.begin(), hash.end()); - try { - vector qualities = prover.GetQualitiesForChallenge(hash.data()); + vector qualities = prover.GetQualitiesForChallenge(hash.data()); - for (uint32_t i = 0; i < qualities.size(); i++) { + for (uint32_t i = 0; i < qualities.size(); i++) { + try { LargeBits proof = prover.GetFullProof(hash.data(), i, parallel_read); uint8_t *proof_data = new uint8_t[proof.GetSize() / 8]; proof.ToBytes(proof_data); @@ -275,19 +292,22 @@ int main(int argc, char *argv[]) try { success++; } else { cout << "Proof verification failed." << endl; + failures += 1; } delete[] proof_data; + } catch (const std::exception& error) { + cout << "Threw: " << error.what() << endl; + exceptions += 1; } - } catch (const std::exception& error) { - cout << "Threw: " << error.what() << endl; - continue; } } std::cout << "Total success: " << success << "/" << iterations << ", " << (success * 100 / static_cast(iterations)) << "%." << std::endl; + std::cout << "Total failures: " << failures << std::endl; + std::cout << "Exceptions: " << exceptions << std::endl; if (show_progress) { progress(4, 1, 1); } } else { - cout << "Invalid operation. Use create/prove/verify/check" << endl; + cout << "Invalid operation '" << operation << "'. Use create/prove/verify/check" << endl; } return 0; } catch (const cxxopts::OptionException &e) { diff --git a/src/prover_disk.hpp b/src/prover_disk.hpp index 68808abc2..93a52b697 100644 --- a/src/prover_disk.hpp +++ b/src/prover_disk.hpp @@ -24,10 +24,11 @@ #include #include #include -#include #include #include #include +#include +#include #include "../lib/include/picosha2.hpp" #include "calculate_bucket.hpp" @@ -36,6 +37,14 @@ #include "serialize.hpp" #include "util.hpp" +#if USE_GREEN_REAPER + #include "GreenReaperPortable.h" +#endif + +#define CHIA_PLOT_V2_MAGIC 0x544F4C50ul // "PLOT" +#define CHIA_PLOT_VERSION_2_0_0 2 + + struct plot_header { uint8_t magic[19]; uint8_t id[32]; @@ -44,6 +53,229 @@ struct plot_header { uint8_t fmt_desc[50]; }; +#if USE_GREEN_REAPER +static GRApi _grApi{}; +static bool _dcompressor_queue_initialized = false; +class ContextQueue { +public: + ContextQueue() {} + + bool init( + uint32_t context_count, + uint32_t thread_count, + bool no_cpu_affinity, + const uint32_t max_compression_level, + bool use_gpu_harvesting, + uint32_t gpu_index, + bool enforce_gpu_index + ) { + assert(!_dcompressor_queue_initialized); + _dcompressor_queue_initialized = true; + + // Populate the API + #if _WIN32 + #define GR_LIB_PREFIX "" + #define GR_LIB_EXT ".dll" + #else + #define GR_LIB_PREFIX "lib" + + #if __APPLE__ + #define GR_LIB_EXT ".dylib" + #else + #define GR_LIB_EXT ".so" + #endif + #endif + + // void* lib = grLoadModule(GR_LIB_PREFIX "bladebit_harvester" GR_LIB_EXT); + + // if (lib == nullptr) { + // int code; + // #if _WIN32 + // code = (int)::GetLastError(); + // #else + // code = (int)dlerror(); + // #endif + + // std::stringstream err; err << "Failed to load bladebit_harvester with error: '" << code << "'"; + // throw std::runtime_error(err.str()); + // } + // #undef GR_LIB_PREFIX + // #undef GR_LIB_EXT + + // // Init GR API + // { + // const auto r = grPopulateApiFromModule(lib, &_grApi, sizeof(GRApi), GR_API_VERSION); + // if (r != GRResult_OK) { + // std::stringstream err; err << "Failed to initialize GR API with error " << r; + // throw std::runtime_error(err.str()); + // } + // } + + GreenReaperConfig cfg = {}; + cfg.apiVersion = GR_API_VERSION; + cfg.threadCount = thread_count; + cfg.disableCpuAffinity = no_cpu_affinity; + if (!use_gpu_harvesting) { + cfg.gpuRequest = GRGpuRequestKind_None; + } else { + if (enforce_gpu_index) { + cfg.gpuRequest = GRGpuRequestKind_ExactDevice; + } else { + cfg.gpuRequest = GRGpuRequestKind_FirstAvailable; + } + } + cfg.gpuDeviceIndex = gpu_index; + + for (uint32_t i = 0; i < context_count; i++) { + + cfg.cpuOffset = i * thread_count; + GreenReaperContext* gr = nullptr; + auto result = grCreateContext(&gr, &cfg, sizeof(cfg)); + + std::string error_msg; + + if (result == GRResult_OK) { + assert(gr); + queue.push(gr); + + // Preallocate memory required fot the maximum compression level we are supporting initially + result = grPreallocateForCompressionLevel(gr, 32, max_compression_level); + if (result != GRResult_OK) { + std::stringstream err; err << "Failed to preallocate memory for contexts with result " << result; + error_msg = err.str(); + } + } + if (result != GRResult_OK) { + // Destroy contexts that were already created + while (!queue.empty()) { + grDestroyContext( queue.front() ); + queue.pop(); + } + if (error_msg.length() < 1) { + std::stringstream err; err << "Failed to create GRContext with result " << result; + error_msg = err.str(); + } + throw std::runtime_error(error_msg); + } + + if (i == 0 && use_gpu_harvesting) { + if (grHasGpuDecompressor(gr) == GR_TRUE) { + return true; + } else { + // default to CPU + cfg.gpuRequest = GRGpuRequestKind_None; + } + } + } + return false; + } + + void push(GreenReaperContext* gr) { + std::unique_lock lock(mutex); + queue.push(gr); + lock.unlock(); + condition.notify_one(); + } + + GreenReaperContext* pop() { + std::unique_lock lock(mutex); + while (queue.empty()) { + condition.wait(lock); + } + GreenReaperContext* gr = queue.front(); + queue.pop(); + return gr; + } + +private: + std::queue queue; + std::mutex mutex; + std::condition_variable condition; +}; + +class ProofCache { + static constexpr uint32_t MAX_ENTRIES = 64; + + struct Entry { + alignas(16) uint8_t challenge[32]; + uint32_t index; + }; + + uint32_t cache_entry_proof_position = 0; + std::vector challenges; + std::vector full_proofs; + mutable std::mutex lock; + +public: + inline ProofCache() = default; + inline ProofCache(ProofCache&& other) + : cache_entry_proof_position(other.cache_entry_proof_position) + , challenges(std::move(other.challenges)) + , full_proofs(std::move(other.full_proofs)) + { + other.cache_entry_proof_position = 0; + } + + inline ProofCache(ProofCache const& other) = delete; + + inline bool FoundCachedProof(const uint32_t index, const uint8_t* challenge, LargeBits& out_full_proof) { + std::lock_guard l(lock); + + Entry entry; + memcpy(entry.challenge, challenge, sizeof(entry.challenge)); + entry.index = index; + + for (uint32_t i = 0; i < challenges.size(); i++) { + if (memcmp(&challenges[i], &entry, sizeof(Entry)) == 0) { + out_full_proof = full_proofs[i]; + return true; + } + } + return false; + } + + inline void CacheProof(const uint32_t index, const uint8_t* challenge, const LargeBits& full_proof) { + std::lock_guard l(lock); + + Entry entry; + memcpy(entry.challenge, challenge, sizeof(entry.challenge)); + entry.index = index; + + if (challenges.size() < MAX_ENTRIES) { + challenges.emplace_back(entry); + full_proofs.emplace_back(full_proof); + } else { + challenges[cache_entry_proof_position] = entry; + full_proofs[cache_entry_proof_position] = full_proof; + } + cache_entry_proof_position = (cache_entry_proof_position + 1) % MAX_ENTRIES; + } + + static_assert(alignof(ProofCache::Entry) == 16); +}; +#else +// Dummy one for python +class ContextQueue { +public: + inline ContextQueue() {} + + inline bool init( + uint32_t context_count, + uint32_t thread_count, + bool no_cpu_affinity, + const uint32_t max_compression_level, + bool use_gpu_harvesting, + uint32_t gpu_index, + bool enforce_gpu_index + ) + { + return false; + } +}; +#endif // USE_GREEN_REAPER + +ContextQueue decompressor_context_queue; + // The DiskProver, given a correctly formatted plot file, can efficiently generate valid proofs // of space, for a given challenge. @@ -55,6 +287,7 @@ class DiskProver { explicit DiskProver(const std::string& filename) : id(kIdLen) { struct plot_header header{}; + this->compression_level = 0; this->filename = filename; std::ifstream disk_file(filename, std::ios::in | std::ios::binary); @@ -70,27 +303,76 @@ class DiskProver { // 2 bytes - memo length // x bytes - memo - SafeRead(disk_file, (uint8_t*)&header, sizeof(header)); - if (memcmp(header.magic, "Proof of Space Plot", sizeof(header.magic)) != 0) - throw std::invalid_argument("Invalid plot header magic"); + // Check for V2 Magic. + uint8_t magic_2_bytes[4]; + SafeRead(disk_file, magic_2_bytes, 4); + uint32_t magic_2_result; + memcpy(&magic_2_result, magic_2_bytes, sizeof(magic_2_result)); + if (magic_2_result == CHIA_PLOT_V2_MAGIC) { + uint8_t version_bytes[4]; + SafeRead(disk_file, version_bytes, 4); + uint32_t version_result; + memcpy(&version_result, version_bytes, sizeof(version_result)); + if (version_result == CHIA_PLOT_VERSION_2_0_0) { + version = 2; + } + else { + throw std::invalid_argument("Unsupported version."); + } + } else { + // V1 + version = 1; + memcpy(header.magic, magic_2_bytes, sizeof(magic_2_bytes)); + uint8_t tmp_magic_buff[15]; + SafeRead(disk_file, tmp_magic_buff, sizeof(header.magic) - 4); + memcpy(header.magic + 4, tmp_magic_buff, sizeof(tmp_magic_buff)); + if (memcmp(header.magic, "Proof of Space Plot", sizeof(header.magic)) != 0) { + throw std::invalid_argument("Invalid plot header magic: " + Util::HexStr(header.magic, 19)); + } + } - uint16_t fmt_desc_len = Util::TwoBytesToInt(header.fmt_desc_len); + SafeRead(disk_file, (uint8_t*)&header.id, sizeof(header.id)); + SafeRead(disk_file, (uint8_t*)&header.k, sizeof(header.k)); - if (fmt_desc_len == kFormatDescription.size() && - !memcmp(header.fmt_desc, kFormatDescription.c_str(), fmt_desc_len)) { - // OK - } else { - throw std::invalid_argument("Invalid plot file format"); + if (version == 1) { + SafeRead(disk_file, (uint8_t*)&header.fmt_desc_len, sizeof(header.fmt_desc_len)); + SafeRead(disk_file, (uint8_t*)&header.fmt_desc, sizeof(header.fmt_desc)); + + uint16_t fmt_desc_len = Util::TwoBytesToInt(header.fmt_desc_len); + + if (fmt_desc_len == kFormatDescription.size() && + !memcmp(header.fmt_desc, kFormatDescription.c_str(), fmt_desc_len)) { + // OK + } else { + throw std::invalid_argument("Invalid plot file format"); + } + SafeSeek(disk_file, offsetof(struct plot_header, fmt_desc) + fmt_desc_len); } + memcpy(id.data(), header.id, sizeof(header.id)); this->k = header.k; - SafeSeek(disk_file, offsetof(struct plot_header, fmt_desc) + fmt_desc_len); uint8_t size_buf[2]; SafeRead(disk_file, size_buf, 2); memo.resize(Util::TwoBytesToInt(size_buf)); SafeRead(disk_file, memo.data(), memo.size()); + if (version == 2) { + uint8_t flags_bytes[4]; + SafeRead(disk_file, flags_bytes, sizeof(flags_bytes)); + uint32_t flags; + memcpy(&flags, flags_bytes, sizeof(flags)); + if (flags & 1) { + uint8_t compression_level; + SafeRead(disk_file, &compression_level, sizeof(compression_level)); + this->compression_level = compression_level; + } + } + #if !defined( USE_GREEN_REAPER ) + if (this->compression_level > 0) + throw std::logic_error("Harvester does not support compressed plots."); + #endif + this->table_begin_pointers = std::vector(11, 0); this->C2 = std::vector(); @@ -134,7 +416,7 @@ class DiskProver { { Deserializer deserializer(vecBytes); deserializer >> version; - if (version != VERSION) { + if (version != 1 && version != 2) { // TODO: Migrate to new version if we change something related to the data structure throw std::invalid_argument("DiskProver: Invalid version."); } @@ -144,16 +426,30 @@ class DiskProver { deserializer >> k; deserializer >> table_begin_pointers; deserializer >> C2; + if (version == 2) { + deserializer >> compression_level; + } else { + compression_level = 0; + } + + #if !defined( USE_GREEN_REAPER ) + if (compression_level > 0) + throw std::runtime_error("Harvester does not support compressed plots."); + #endif } DiskProver(DiskProver const&) = delete; DiskProver(DiskProver&& other) noexcept + #if USE_GREEN_REAPER + : cached_proofs(std::move(other.cached_proofs)) + #endif { filename = std::move(other.filename); memo = std::move(other.memo); id = std::move(other.id); k = other.k; + compression_level = other.compression_level; table_begin_pointers = std::move(other.table_begin_pointers); C2 = std::move(other.C2); version = std::move(other.version); @@ -180,6 +476,56 @@ class DiskProver { uint8_t GetSize() const noexcept { return k; } + uint8_t GetCompressionLevel() const noexcept { return compression_level; } + + bool CompareProofBits(const LargeBits& left, const LargeBits& right, uint8_t k) + { + uint16_t size = left.GetSize() / k; + assert(left.GetSize() == right.GetSize()); + for (int16_t i = size - 1; i >= 0; i--) { + LargeBits left_val = left.Slice(k * i, k * (i + 1)); + LargeBits right_val = right.Slice(k * i, k * (i + 1)); + if (left_val < right_val) { + return true; + } + if (left_val > right_val) { + return false; + } + } + return false; + } + + LargeBits GetQualityStringFromProof( + LargeBits proof, + const uint8_t* challenge) + { + Bits challenge_bits = Bits(challenge, 256 / 8, 256); + uint16_t quality_index = challenge_bits.Slice(256 - 5).GetValue() << 1; + + // Converts the proof from proof ordering to plot ordering + for (uint8_t table_index = 1; table_index < 7; table_index++) { + LargeBits new_proof; + uint16_t size = k * (1 << (table_index - 1)); + for (int j = 0; j < (1 << (7 - table_index)); j += 2) { + LargeBits L = proof.Slice(j * size, (j + 1) * size); + LargeBits R = proof.Slice((j + 1) * size, (j + 2) * size); + if (CompareProofBits(L, R, k)) { + new_proof += (L + R); + } else { + new_proof += (R + L); + } + } + proof = new_proof; + } + // Hashes two of the x values, based on the quality index + std::vector hash_input(32 + Util::ByteAlign(2 * k) / 8, 0); + memcpy(hash_input.data(), challenge, 32); + proof.Slice(k * quality_index, k * (quality_index + 2)).ToBytes(hash_input.data() + 32); + std::vector hash(picosha2::k_digest_size); + picosha2::hash256(hash_input.begin(), hash_input.end(), hash.begin(), hash.end()); + return LargeBits(hash.data(), 32, 256); + } + // Given a challenge, returns a quality string, which is sha256(challenge + 2 adjecent x // values), from the 64 value proof. Note that this is more efficient than fetching all 64 x // values, which are in different parts of the disk. @@ -187,9 +533,10 @@ class DiskProver { { std::vector qualities; - std::lock_guard l(_mtx); + uint32_t p7_entries_size = 0; { + std::lock_guard l(_mtx); std::ifstream disk_file(filename, std::ios::in | std::ios::binary); if (!disk_file.is_open()) { @@ -203,15 +550,22 @@ class DiskProver { if (p7_entries.empty()) { return std::vector(); } + p7_entries_size = p7_entries.size(); // The last 5 bits of the challenge determine which route we take to get to // our two x values in the leaves. uint8_t last_5_bits = challenge[31] & 0x1f; for (uint64_t position : p7_entries) { + #if USE_GREEN_REAPER + if (compression_level >= 9) { + break; + } + #endif // This inner loop goes from table 6 to table 1, getting the two backpointers, // and following one of them. - for (uint8_t table_index = 6; table_index > 1; table_index--) { + uint64_t alt_position; + for (uint8_t table_index = 6; table_index > GetEndTable(); table_index--) { uint128_t line_point = ReadLinePoint(disk_file, table_index, position); auto xy = Encoding::LinePointToSquare(line_point); @@ -219,13 +573,47 @@ class DiskProver { if (((last_5_bits >> (table_index - 2)) & 1) == 0) { position = xy.second; + alt_position = xy.first; } else { position = xy.first; + alt_position = xy.second; } } - uint128_t new_line_point = ReadLinePoint(disk_file, 1, position); - auto x1x2 = Encoding::LinePointToSquare(new_line_point); - + uint128_t new_line_point = ReadLinePoint(disk_file, GetEndTable(), position); + std::pair x1x2; + + #if USE_GREEN_REAPER + if (compression_level > 0) { + GRCompressedQualitiesRequest req; + req.compressionLevel = compression_level; + req.plotId = id.data(); + req.challenge = challenge; + req.xLinePoints[0].hi = (uint64_t)(new_line_point >> 64); + req.xLinePoints[0].lo = (uint64_t)new_line_point; + if (compression_level >= 6) { + uint128_t alt_line_point = ReadLinePoint(disk_file, GetEndTable(), alt_position); + req.xLinePoints[1].hi = (uint64_t)(alt_line_point >> 64); + req.xLinePoints[1].lo = (uint64_t)alt_line_point; + } + + GreenReaperContext* gr = decompressor_context_queue.pop(); + assert(gr); + + auto res = grGetFetchQualitiesXPair(gr, &req); + decompressor_context_queue.push(gr); + + if (res != GRResult_OK) { + // Expect this will result in failure in a later step. + x1x2.first = x1x2.second = 0; + } else { + x1x2.first = req.x1; + x1x2.second = req.x2; + } + } else + #endif // #if USE_GREEN_REAPER + { + x1x2 = Encoding::LinePointToSquare(new_line_point); + } // The final two x values (which are stored in the same location) are hashed std::vector hash_input(32 + Util::ByteAlign(2 * k) / 8, 0); memcpy(hash_input.data(), challenge, 32); @@ -236,6 +624,23 @@ class DiskProver { qualities.emplace_back(hash.data(), 32, 256); } } // Scope for disk_file + + #if USE_GREEN_REAPER + if (compression_level >= 9) { + uint8_t failure_bytes[32]; + for (int i = 0; i < 32; i++) { + failure_bytes[i] = 255; + } + for (uint32_t i = 0; i < p7_entries_size; i++) { + try { + auto proof = GetFullProof(challenge, i); + qualities.push_back(GetQualityStringFromProof(proof, challenge)); + } catch (const std::exception& error) { + qualities.emplace_back(failure_bytes, 32, 256); + } + } + } + #endif return qualities; } @@ -245,6 +650,12 @@ class DiskProver { LargeBits GetFullProof(const uint8_t* challenge, uint32_t index, bool parallel_read = true) { LargeBits full_proof; + + #if USE_GREEN_REAPER + if (compression_level >= 9 && cached_proofs.FoundCachedProof(index, challenge, full_proof)) { + return full_proof; + } + #endif std::lock_guard l(_mtx); { @@ -262,11 +673,55 @@ class DiskProver { // Gets the 64 leaf x values, concatenated together into a k*64 bit string. std::vector xs; if (parallel_read) { - xs = GetInputs(p7_entries[index], 6); + xs = GetInputs(p7_entries[index], 6, nullptr); } else { xs = GetInputs(p7_entries[index], 6, &disk_file); // Passing in a disk_file disabled the parallel reads } + #if USE_GREEN_REAPER + if (compression_level > 0) { + auto gr = decompressor_context_queue.pop(); + + GRCompressedProofRequest req{}; + req.compressionLevel = compression_level; + req.plotId = id.data(); + + uint8_t compressed_proof_size = (compression_level <= 8 ? GR_POST_PROOF_CMP_X_COUNT : (GR_POST_PROOF_CMP_X_COUNT / 2)); + for (int i = 0; i < compressed_proof_size; i++) { + req.compressedProof[i] = xs[i].GetValue(); + } + + GRResult res = grFetchProofForChallenge(gr, &req); + decompressor_context_queue.push(gr); + + if (res != GRResult_OK) { + if (res == GRResult_NoProof) { + throw std::runtime_error("GRResult_NoProof received"); + } + if (res == GRResult_Failed) { + throw std::runtime_error("GRResult is not GRResult_OK, received GRResult_Failed"); + } + if (res == GRResult_OutOfMemory) { + throw std::runtime_error("GRResult is not GRResult_OK, received GRResult_OutOfMemory"); + } + if (res == GRResult_WrongVersion) { + throw std::runtime_error("GRResult is not GRResult_OK, received GRResult_WrongVersion"); + } + if (res == GRResult_InvalidGPU) { + throw std::runtime_error("GRResult is not GRResult_OK, received GRResult_InvalidGPU"); + } + if (res == GRResult_InvalidArg) { + throw std::runtime_error("GRResult is not GRResult_OK, received GRResult_InvalidArg"); + } + } + std::vector uncompressed_xs; + for (int i = 0; i < GR_POST_PROOF_X_COUNT; i++) { + uncompressed_xs.push_back(Bits(req.fullProof[i], k)); + } + xs = uncompressed_xs; + } + #endif + // Sorts them according to proof ordering, where // f1(x0) m= f1(x1), f2(x0, x1) m= f2(x2, x3), etc. On disk, they are not stored in // proof ordering, they're stored in plot ordering, due to the sorting in the Compress @@ -276,6 +731,12 @@ class DiskProver { full_proof += x; } } // Scope for disk_file + + #if USE_GREEN_REAPER + if (compression_level >= 9) { + cached_proofs.CacheProof(index, challenge, full_proof); + } + #endif return full_proof; } @@ -283,6 +744,9 @@ class DiskProver { { Serializer serializer; serializer << version << filename << memo << id << k << table_begin_pointers << C2; + if (version == 2) { + serializer << compression_level; + } return serializer.Data(); } @@ -293,8 +757,12 @@ class DiskProver { std::vector memo; std::vector id; // Unique plot id uint8_t k; + uint8_t compression_level; std::vector table_begin_pointers; std::vector C2; + #if USE_GREEN_REAPER + ProofCache cached_proofs; + #endif // Using this method instead of simply seeking will prevent segfaults that would arise when // continuing the process of looking up qualities. @@ -328,14 +796,53 @@ class DiskProver { } } + uint8_t GetEndTable() { + if (compression_level == 0) { + return 1; + } + if (compression_level <= 8) { + return 2; + } + return 3; + } + // Reads exactly one line point (pair of two k bit back-pointers) from the given table. // The entry at index "position" is read. First, the park index is calculated, then // the park is read, and finally, entry deltas are added up to the position that we // are looking for. - uint128_t ReadLinePoint(std::ifstream& disk_file, uint8_t table_index, uint64_t position) - { + uint128_t ReadLinePoint( + std::ifstream& disk_file, + uint8_t table_index, + uint64_t position + ) { + size_t compressed_park_size = 0; + uint32_t compressed_stub_size_bits = 0; + double compressed_ans_r_value = 0; + + const bool is_compressed = compression_level > 0 && table_index == GetEndTable(); + (void)is_compressed; + + #if USE_GREEN_REAPER + if (is_compressed) { + GRCompressionInfo info{}; + const auto r = grGetCompressionInfo(&info, sizeof(info), k, compression_level); + if (r != GRResult_OK) { + std::stringstream err; err << "Failed to obtain compression info with error " << r; + throw std::runtime_error(err.str()); + } + + compressed_park_size = info.tableParkSize;; + compressed_stub_size_bits = info.subtSizeBits; + compressed_ans_r_value = info.ansRValue; + } + #else + (void)compressed_stub_size_bits; + (void)compressed_ans_r_value; + (void)compressed_park_size; + #endif + uint64_t park_index = position / kEntriesPerPark; - uint32_t park_size_bits = EntrySizes::CalculateParkSize(k, table_index) * 8; + uint32_t park_size_bits = (is_compressed ? compressed_park_size : EntrySizes::CalculateParkSize(k, table_index)) * 8; SafeSeek(disk_file, table_begin_pointers[table_index] + (park_size_bits / 8) * park_index); @@ -346,12 +853,12 @@ class DiskProver { uint128_t line_point = Util::SliceInt128FromBytes(line_point_bin, 0, k * 2); // Reads EPP stubs - uint32_t stubs_size_bits = EntrySizes::CalculateStubsSize(k) * 8; + uint32_t stubs_size_bits = (is_compressed ? (Util::ByteAlign((kEntriesPerPark - 1) * compressed_stub_size_bits) / 8) : EntrySizes::CalculateStubsSize(k)) * 8; auto* stubs_bin = new uint8_t[stubs_size_bits / 8 + 7]; SafeRead(disk_file, stubs_bin, stubs_size_bits / 8); - // Reads EPP deltas - uint32_t max_deltas_size_bits = EntrySizes::CalculateMaxDeltasSize(k, table_index) * 8; + // Reads EPP deltas + uint32_t max_deltas_size_bits = (is_compressed ? compressed_park_size - (line_point_size + stubs_size_bits) : EntrySizes::CalculateMaxDeltasSize(k, table_index)) * 8; auto* deltas_bin = new uint8_t[max_deltas_size_bits / 8]; // Reads the size of the encoded deltas object @@ -374,13 +881,13 @@ class DiskProver { SafeRead(disk_file, deltas_bin, encoded_deltas_size); // Decodes the deltas - double R = kRValues[table_index - 1]; + double R = (is_compressed ? compressed_ans_r_value : kRValues[table_index - 1]); deltas = Encoding::ANSDecodeDeltas(deltas_bin, encoded_deltas_size, kEntriesPerPark - 1, R); } uint32_t start_bit = 0; - uint8_t stub_size = k - kStubMinusBits; + uint8_t stub_size = (uint8_t)(is_compressed ? compressed_stub_size_bits : k - kStubMinusBits); uint64_t sum_deltas = 0; uint64_t sum_stubs = 0; for (uint32_t i = 0; @@ -724,7 +1231,7 @@ class DiskProver { } std::pair xy = Encoding::LinePointToSquare(line_point); - if (depth == 1) { + if (depth == GetEndTable()) { // For table P1, the line point represents two concatenated x values. std::vector ret; ret.emplace_back(xy.second, k); // y