diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 94eb5f4a4..9590b69ed 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -12,10 +12,12 @@ jobs: build-windows: name: Windows runs-on: windows-2022 + permissions: + contents: write env: BOOST_ROOT: C:/local/boost_1_83_0 steps: - - uses: actions/checkout@master + - uses: actions/checkout@v5 - name: Prepare version shell: powershell @@ -27,10 +29,10 @@ jobs: echo "release_name=${release_name}" >> $env:GITHUB_OUTPUT - name: Install msbuild - uses: microsoft/setup-msbuild@v2 + uses: microsoft/setup-msbuild@v3 - name: Restore Boost - uses: actions/cache@v4 + uses: actions/cache@v5 id: restore-boost with: path: ${{env.BOOST_ROOT}} @@ -64,7 +66,7 @@ jobs: cp build/tests/Release/*_tests.exe build/conceal - name: Upload To GH Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ${{ steps.setup.outputs.release_name }} path: build/conceal @@ -99,11 +101,13 @@ jobs: build-mingw: name: MinGW runs-on: windows-2022 + permissions: + contents: write defaults: run: shell: msys2 {0} steps: - - uses: actions/checkout@master + - uses: actions/checkout@v5 - uses: msys2/setup-msys2@v2 with: @@ -148,7 +152,7 @@ jobs: cp build/tests/*_tests.exe build/conceal - name: Upload To GH Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ${{ steps.setup.outputs.release_name }} path: build/conceal @@ -183,8 +187,10 @@ jobs: build-ubuntu22: name: Ubuntu 22.04 runs-on: ubuntu-22.04 + permissions: + contents: write steps: - - uses: actions/checkout@master + - uses: actions/checkout@v5 - name: Prepare version id: setup @@ -217,7 +223,7 @@ jobs: cp build/tests/*_tests build/conceal - name: Upload To GH Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ${{ steps.setup.outputs.release_name }} path: build/conceal @@ -253,8 +259,10 @@ jobs: build-ubuntu24: name: Ubuntu 24.04 runs-on: ubuntu-24.04 + permissions: + contents: write steps: - - uses: actions/checkout@master + - uses: actions/checkout@v5 - name: Prepare version id: setup @@ -287,7 +295,7 @@ jobs: cp build/tests/*_tests build/conceal - name: Upload To GH Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ${{ steps.setup.outputs.release_name }} path: build/conceal @@ -323,8 +331,10 @@ jobs: build-ubuntu22-clang: name: Ubuntu 22.04 clang runs-on: ubuntu-22.04 + permissions: + contents: write steps: - - uses: actions/checkout@master + - uses: actions/checkout@v5 - name: Prepare version id: setup @@ -357,7 +367,7 @@ jobs: cp build/tests/*_tests build/conceal - name: Upload To GH Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ${{ steps.setup.outputs.release_name }} path: build/conceal @@ -392,28 +402,31 @@ jobs: build-macos: name: macOS - runs-on: macos-13 + runs-on: macos-15-intel + permissions: + contents: write steps: - - uses: actions/checkout@master + - uses: actions/checkout@v5 - name: Prepare version id: setup run: | - os=macos-13 + os=macos-15-intel ccx_version=${GITHUB_SHA::7} release_name=ccx-cli-"$os"-dev-"$ccx_version" echo "release_name=${release_name}" >> $GITHUB_OUTPUT - name: Install dependencies run: | - brew install boost@1.85 + brew install boost + brew list --versions boost - name: Build id: build run: | mkdir build cd build - cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=ON -DSTATIC=ON -DBOOST_ROOT=/usr/local/opt/boost@1.85 + cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=ON -DSTATIC=ON -DBOOST_ROOT="$(brew --prefix boost)" make -j2 - name: Prepare release @@ -426,7 +439,7 @@ jobs: cp build/tests/*_tests build/conceal - name: Upload To GH Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ${{ steps.setup.outputs.release_name }} path: build/conceal diff --git a/.github/workflows/macOS.yml b/.github/workflows/macOS.yml index 03aab0c24..e74487bcd 100644 --- a/.github/workflows/macOS.yml +++ b/.github/workflows/macOS.yml @@ -4,13 +4,15 @@ on: push: tags: - "*" - + jobs: build-macos: name: macOS - runs-on: macos-13 + runs-on: macos-15-intel + permissions: + contents: write steps: - - uses: actions/checkout@master + - uses: actions/checkout@v5 - name: Build id: build @@ -36,7 +38,7 @@ jobs: echo "ccx_version=${ccx_version}" >> $GITHUB_OUTPUT - name: Create Release - uses: softprops/action-gh-release@v2.0.4 + uses: softprops/action-gh-release@v3 with: files: ${{ steps.build.outputs.asset_path }} name: Conceal Core CLI v${{ steps.build.outputs.ccx_version }} diff --git a/.github/workflows/ubuntu22.yml b/.github/workflows/ubuntu22.yml index e82b4d8b0..9bcc377c6 100644 --- a/.github/workflows/ubuntu22.yml +++ b/.github/workflows/ubuntu22.yml @@ -9,8 +9,10 @@ jobs: build-ubuntu22: name: Ubuntu 22.04 runs-on: ubuntu-22.04 + permissions: + contents: write steps: - - uses: actions/checkout@master + - uses: actions/checkout@v5 - name: Build id: build @@ -43,7 +45,7 @@ jobs: echo "ccx_version=${ccx_version}" >> $GITHUB_OUTPUT - name: Create Release - uses: softprops/action-gh-release@v2.0.4 + uses: softprops/action-gh-release@v3 with: files: ${{ steps.build.outputs.asset_path }} name: Conceal Core CLI v${{ steps.build.outputs.ccx_version }} diff --git a/.github/workflows/ubuntu24.yml b/.github/workflows/ubuntu24.yml index 2e2e338f6..9849e0673 100644 --- a/.github/workflows/ubuntu24.yml +++ b/.github/workflows/ubuntu24.yml @@ -4,13 +4,15 @@ on: push: tags: - "*" - + jobs: build-ubuntu24: name: Ubuntu 24.04 runs-on: ubuntu-24.04 + permissions: + contents: write steps: - - uses: actions/checkout@master + - uses: actions/checkout@v5 - name: Build id: build @@ -43,7 +45,7 @@ jobs: echo "ccx_version=${ccx_version}" >> $GITHUB_OUTPUT - name: Create Release - uses: softprops/action-gh-release@v2.0.4 + uses: softprops/action-gh-release@v3 with: files: ${{ steps.build.outputs.asset_path }} name: Conceal Core CLI v${{ steps.build.outputs.ccx_version }} diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 38b3522a7..8dbfe7e7e 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -9,13 +9,15 @@ jobs: build-windows: name: Windows runs-on: windows-2022 + permissions: + contents: write env: BOOST_ROOT: C:/local/boost_1_83_0 steps: - - uses: actions/checkout@master + - uses: actions/checkout@v5 - name: Setup msbuild - uses: microsoft/setup-msbuild@v2 + uses: microsoft/setup-msbuild@v3 - name: Restore Boost uses: actions/cache@v4 @@ -55,7 +57,7 @@ jobs: echo "ccx_version=${ccx_version}" >> $env:GITHUB_OUTPUT - name: Create Release - uses: softprops/action-gh-release@v2.0.4 + uses: softprops/action-gh-release@v3 with: files: ${{ steps.build.outputs.asset_path }} name: Conceal Core CLI v${{ steps.build.outputs.ccx_version }} diff --git a/.gitignore b/.gitignore index f42fcf92a..efb6ba307 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ compile_commands.json *.json cmake-build* -.cursorrules \ No newline at end of file +.cursorrules +CLAUDE.md \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ac2b6ed9..6b195f9fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.5) include(CheckCXXCompilerFlag) -set(VERSION "6.7.3") +set(VERSION "6.7.4-beta.1") set(VERSION_BUILD_NO "Trebopala") # Packaged from main commits set(COMMIT 1db6e66) @@ -112,7 +112,8 @@ else() endif() if(MINGW) set(WARNINGS "${WARNINGS} -Wno-error=unused-value") - set(MINGW_FLAG "-DWIN32_LEAN_AND_MEAN") + # Boost.Bind: avoid deprecated global placeholders (_1, _2) pragma on newer Boost + set(MINGW_FLAG "-DWIN32_LEAN_AND_MEAN -DBOOST_BIND_GLOBAL_PLACEHOLDERS") include_directories(SYSTEM src/platform/mingw) else() set(MINGW_FLAG "") @@ -144,6 +145,10 @@ if(APPLE) add_definitions(/DHAVE_ROTR) endif() +if(POLICY CMP0167) + cmake_policy(SET CMP0167 OLD) +endif() + if(STATIC) set(Boost_NO_BOOST_CMAKE ON) set(Boost_USE_STATIC_LIBS ON) @@ -151,7 +156,17 @@ if(STATIC) endif() #set(Boost_DEBUG on) -find_package(Boost 1.55 REQUIRED COMPONENTS system filesystem thread date_time chrono regex serialization program_options) +set(BOOST_COMPONENTS filesystem thread date_time chrono regex serialization program_options) + +if(APPLE) + find_package(Boost 1.55 COMPONENTS system ${BOOST_COMPONENTS}) + if(NOT Boost_FOUND) + message(STATUS "Boost.System library not found; retrying without system component") + find_package(Boost 1.55 REQUIRED COMPONENTS ${BOOST_COMPONENTS}) + endif() +else() + find_package(Boost 1.55 REQUIRED COMPONENTS system ${BOOST_COMPONENTS}) +endif() include_directories(SYSTEM ${Boost_INCLUDE_DIRS}) if(MINGW) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab99bfa93..300b2bc7f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,14 @@ Development Process -Developers work in their own trees, then submit pull requests when they think their feature or bug fix is ready. +Contributors should work from their own fork of the repository (not push branches directly to the upstream repo unless they are maintainers with that workflow). When you start a change, create a branch named `/`: use a three-letter identifier derived from your name or handle, then a short topic (often kebab-case words). Examples for someone named John Doe: `jdo/fix`, `doe/dependencies`. + +Open pull requests against the upstream **`development`** branch when you consider your feature or bug fix ready. The patch will be accepted if there is broad consensus that it is a good thing. Developers should expect to rework and resubmit patches if they don't match the project's coding conventions or are controversial. -The master branch is regularly built and tested, but is not guaranteed to be completely stable. Tags are regularly created to indicate new official, stable release versions of Conceal. +The `development` branch is regularly built and tested, but is not guaranteed to be completely stable. Tags are regularly created to indicate new official, stable release versions of Conceal. -Feature branches are created when there are major new features being worked on by several people. +Feature branches on upstream may be created when there are major new features being worked on by several people. From time to time a pull request will become outdated. If this occurs, and the pull is no longer automatically mergeable; a comment on the pull will be used to issue a warning of closure. The pull will be closed 15 days after the warning if action is not taken by the author. Pull requests closed in this manner will have their corresponding issue labeled 'stagnant'. diff --git a/COPYING.md b/COPYING.md deleted file mode 100644 index aa6404ce4..000000000 --- a/COPYING.md +++ /dev/null @@ -1,24 +0,0 @@ -MIT License -Distributed under the MIT/X11 software license http://www.opensource.org/licenses/mit-license.php - -Copyright (c) 2017-2023 Conceal Community -Copyright (c) 2017-2018 The Circle Foundation & Conceal Devs -Copyright (c) 2018-2023 Conceal Network & Conceal Devs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/COPYRIGHT b/COPYRIGHT deleted file mode 100644 index aa6404ce4..000000000 --- a/COPYRIGHT +++ /dev/null @@ -1,24 +0,0 @@ -MIT License -Distributed under the MIT/X11 software license http://www.opensource.org/licenses/mit-license.php - -Copyright (c) 2017-2023 Conceal Community -Copyright (c) 2017-2018 The Circle Foundation & Conceal Devs -Copyright (c) 2018-2023 Conceal Network & Conceal Devs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LICENSE b/LICENSE index aa6404ce4..f9f447e7a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,8 @@ MIT License -Distributed under the MIT/X11 software license http://www.opensource.org/licenses/mit-license.php -Copyright (c) 2017-2023 Conceal Community +Copyright (c) 2011-2017 The Cryptonote developers Copyright (c) 2017-2018 The Circle Foundation & Conceal Devs -Copyright (c) 2018-2023 Conceal Network & Conceal Devs +Copyright (c) 2018-2024 Conceal Network & Conceal Devs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -21,4 +20,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 9099320f2..1a58ec5fe 100644 --- a/README.md +++ b/README.md @@ -167,4 +167,4 @@ If the build is successful the binaries will be located in the `src` folder. Special thanks goes out to the developers from Cryptonote, Bytecoin, Ryo, Monero, Forknote, TurtleCoin, Karbo and Masari. -Copyright (c) 2017-2023 Conceal Community, Conceal Network & Conceal Devs \ No newline at end of file +Copyright (c) 2017-2024 Conceal Community, Conceal Network & Conceal Devs \ No newline at end of file diff --git a/external/parallel_hashmap/phmap.h b/external/parallel_hashmap/phmap.h index 37f0f7c14..194e4f600 100644 --- a/external/parallel_hashmap/phmap.h +++ b/external/parallel_hashmap/phmap.h @@ -2753,7 +2753,7 @@ class parallel_hash_set std::is_nothrow_default_constructible::value&& std::is_nothrow_default_constructible::value) {} -#if (__cplusplus >= 201703L || _MSVC_LANG >= 201402) && (defined(_MSC_VER) || defined(__clang__) || (defined(__GNUC__) && __GNUC__ > 6)) +#if (__cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201402)) && (defined(_MSC_VER) || defined(__clang__) || (defined(__GNUC__) && __GNUC__ > 6)) explicit parallel_hash_set(size_t bucket_cnt, const hasher& hash_param = hasher(), const key_equal& eq = key_equal(), diff --git a/src/ConcealWallet/ConcealWallet.cpp b/src/ConcealWallet/ConcealWallet.cpp index 7e0fec321..4378031ce 100644 --- a/src/ConcealWallet/ConcealWallet.cpp +++ b/src/ConcealWallet/ConcealWallet.cpp @@ -770,12 +770,16 @@ bool conceal_wallet::reset(const std::vector &) m_wallet->reset(0); m_wallet->addObserver(this); - success_msg_writer(true) << "Reset completed successfully."; + success_msg_writer(true) << "Attempting wallet reset... this could take some time."; std::unique_lock lock(m_walletSynchronizedMutex); m_walletSynchronizedCV.wait(lock, [this] { return m_walletSynchronized; }); + if (m_walletSynchronized) { + success_msg_writer(true) << "Wallet reset was successful"; + } + std::cout << std::endl; return true; @@ -1833,4 +1837,4 @@ void conceal_wallet::depositUpdated(DepositId depositId) void conceal_wallet::depositsUpdated(const std::vector &depositIds) { // Nothing to do here -} \ No newline at end of file +} diff --git a/src/CryptoNoteConfig.h b/src/CryptoNoteConfig.h index 4bde41c38..c62ee2dd0 100644 --- a/src/CryptoNoteConfig.h +++ b/src/CryptoNoteConfig.h @@ -1,6 +1,6 @@ // Copyright (c) 2012-2017 The Cryptonote developers // Copyright (c) 2017-2018 The Circle Foundation & Conceal Devs -// Copyright (c) 2018-2023 Conceal Network & Conceal Devs +// Copyright (c) 2018-2026 Conceal Network & Conceal Devs // // // Distributed under the MIT/X11 software license, see the accompanying @@ -141,7 +141,8 @@ namespace cn const char P2P_NET_DATA_FILENAME[] = "p2pstate.bin"; const char CRYPTONOTE_BLOCKCHAIN_INDICES_FILENAME[] = "blockchainindices.dat"; const char MINER_CONFIG_FILE_NAME[] = "miner_conf.json"; - + const char CRYPTONOTE_CHECKPOINT_FILENAME[] = "checkpoint.dat"; + } // namespace parameters const uint64_t START_BLOCK_REWARD = (UINT64_C(5000) * parameters::POINT); // start reward (Consensus I) @@ -185,13 +186,15 @@ namespace cn and the minimum version for communication between nodes */ const uint8_t P2P_VERSION_1 = 1; const uint8_t P2P_VERSION_2 = 2; - const uint8_t P2P_CURRENT_VERSION = 1; + const uint8_t P2P_CURRENT_VERSION = 2; // Incremented to support checkpoint list propagation const uint8_t P2P_MINIMUM_VERSION = 1; const uint8_t P2P_UPGRADE_WINDOW = 2; // This defines the minimum P2P version required for lite blocks propogation const uint8_t P2P_LITE_BLOCKS_PROPOGATION_VERSION = 3; + const uint8_t P2P_CHECKPOINT_LIST_VERSION = 2; + const size_t P2P_LOCAL_WHITE_PEERLIST_LIMIT = 1000; const size_t P2P_LOCAL_GRAY_PEERLIST_LIMIT = 5000; @@ -206,6 +209,23 @@ namespace cn const uint32_t P2P_DEFAULT_PING_CONNECTION_TIMEOUT = 2000; // 2 seconds const uint64_t P2P_DEFAULT_INVOKE_TIMEOUT = 60 * 2 * 1000; // 2 minutes const size_t P2P_DEFAULT_HANDSHAKE_INVOKE_TIMEOUT = 5000; // 5 seconds + const size_t P2P_CHECKPOINT_LIST_RE_REQUEST = 300; // 5 minutes + const uint32_t P2P_CHECKPOINT_PEER_MIN_UPTIME_BLOCKS = 2; // Minimum uptime for checkpoint verification (mainnet: 2 blocks × 2 min = 4 min) + const uint32_t P2P_CHECKPOINT_PEER_MIN_UPTIME_BLOCKS_TESTNET = 2; // Minimum uptime for checkpoint verification (testnet: 2 blocks × 2 min = 4 min relaxed for testing) + + // Checkpoint consensus configuration (Mainnet) + // M = minimum agreements required (all M peers must agree) + // K = total peers to sample + // n = minimum distinct /16 networks required + const uint32_t CKPT_MIN_CONSENSUS_PEERS = 6; // M: minimum agreements required + const uint32_t CKPT_CONSENSUS_PEERS = 6; // K: minimun total peers to sample + const uint32_t CKPT_MIN_DIVERSE_NETWORKS = 3; // n: minimum distinct /16 networks required + + // Checkpoint consensus configuration (Testnet - relaxed for smaller network) + const uint32_t CKPT_MIN_CONSENSUS_PEERS_TESTNET = 2; // M: minimum agreements required (testnet) + const uint32_t CKPT_CONSENSUS_PEERS_TESTNET = 2; // K: minimum total peers to sample (testnet) - increased to ask multiple peers + const uint32_t CKPT_MIN_DIVERSE_NETWORKS_TESTNET = 2; // n: minimum distinct /16 networks required (testnet) + const char P2P_STAT_TRUSTED_PUB_KEY[] = "f7061e9a5f0d30549afde49c9bfbaa52ac60afdc46304642b460a9ea34bf7a4e"; // Seed Nodes @@ -220,8 +240,7 @@ namespace cn }; const std::initializer_list TESTNET_SEED_NODES = { - "161.97.145.65:15500", - "161.97.145.65:15501" + "5.189.177.60:15500" }; struct CheckpointData @@ -234,6 +253,9 @@ namespace cn __attribute__((unused)) #endif + const char DNS_CHECKPOINT_DOMAIN[] = "checkpoints.conceal.id"; + const char TESTNET_DNS_CHECKPOINT_DOMAIN[] = "testpoints.conceal.id"; + // Blockchain Checkpoints: // {, ""}, const std::initializer_list @@ -425,7 +447,25 @@ namespace cn {1840000, "b3a95fbf906555264a31906678b299c0851d09628b8d37969a285cb54a614c31"}, {1850000, "041d84a8c1fb68d24e6a9d8fe5aab27c6db654ecaad3b2d541c40d6e9df53feb"}, {1860000, "63642037f4d5d82150c120e776a3b2d2a80f725b4759ffda12c94bd45499fb25"}, - {1870000, "159fa13f6f9f48e2d8344e76ebdfe2ea986b9a72e94325dacbbd3bd73cbdbb41"} + {1870000, "159fa13f6f9f48e2d8344e76ebdfe2ea986b9a72e94325dacbbd3bd73cbdbb41"}, + {1880000, "7e5dd6c104989975b482d7bf277934d8f9252e81f5bc996ed7c54ee95542ade6"}, + {1890000, "c2ded97cfaefc52aa1de9f6ca8f04d2f3c2ab3b48bf572801555e9fa7934f954"}, + {1900000, "50f2fc1af569f1ebb6567b915e2ee1bf72c585f544725e952a303b447faab909"}, + {1910000, "e2f3e1560369c4e457fdeffbe2ca2fbf9db7aa89a5cd6d9d2e96f7ef826690eb"}, + {1920000, "ba318eb902d6a3f7dd4e7116b13c6b1f2cb4e8c9e70acbde3b6a28a1374c6243"}, + {1930000, "6f39f0888c14808710e909fb45c864fdf738cf644e432486d8e01b65dbbc2862"}, + {1940000, "fe774dd97a5b975c02abb18ca48bf6fd3a8048b597520befe1f261855a7ccf3f"}, + {1950000, "6147f276271c55319cf6fc03e86bcaa75ea0a2aab126095a9a9f8a811eb229e1"}, + {1960000, "e5b31d500706cbca7e7eaca4b5762413533c826692c632a1c0a1a0df6771f42d"}, + {1970000, "5b8da5814d2d82bd21db65b2b3c78b0f2038deab2ae16eee721193713c9e2e27"}, + {1980000, "a69211b77326d9ff74fe8be81c281e0a44b0c8bbee643cd6d736e186362fd7f5"}, + {1990000, "fe9b87f2e98f544af8b7ac950b7b5a1eebf0a9508ea613c74bd3304433726b30"}, + {2000000, "bf1e1396f4ee1c21a2d574035c6b422d62b8aa3d4b3268111b1b73357ad49740"}, + {2010000, "c4806687630ae3fc1c371889617f0659f2622ba9bcdcee1a0c349dea87ed4f84"}, + {2020000, "ab632a9b914875a2b30bbf5946859c352801450df35e1fdaee8107da3d1df64e"}, + {2030000, "c0b03673083146e028c609498a95e0f22ae61f06f7c961cd1ac5d8ba99c8e3c6"}, + {2040000, "f6abea5fb93bb391f24599252656d40c1d716cb18a245b200f6b70d962bf2bdf"}, + {2050000, "ab2df8795e771bb9b74e8a893f91673f9f44321844ed11d31e11270c09c6e6ae"} }; const std::initializer_list TESTNET_CHECKPOINTS = { @@ -467,7 +507,14 @@ namespace cn {875000, "91eac54f608c1d0e43111c107a2f5caf259bcbbd66714b6591cedbc64f9c4cdf"}, {900000, "a70b6df1794a6d91071cd5fc87719769bf09610d520c2c2134f53908d1e3de40"}, {925000, "a00b47f3610cfd5c509183322fca89e388c0427601ce4db7e397acbcab5a3ee6"}, - {950000, "387573b7b9bdbc1d79c28156cf15d7e08ddf248a0257b0ee7ef2731c5c7a0534"} + {950000, "387573b7b9bdbc1d79c28156cf15d7e08ddf248a0257b0ee7ef2731c5c7a0534"}, + {975000, "2bbf6d2fecb329d9c34968e60b0d2814a2d5a2f69ee872b77d152ba795e881ae"}, + {1000000, "c2627d61bd357fe38398e6fbc14c5c8510aac12934fc696bf7d7935a762f03a1"}, + {1025000, "325a7b27a91d221c58597e17a913b5bf6c79704c1b45a7849ba5d745c6f82e04"}, + {1050000, "ed30311a19f916184da2e961b7cf527114954dc954817804cc49c5f010632dea"}, + {1075000, "0da0f9185f7f32e9ef29eaebe290e1614cb85987e4fb6ba027570be3d2cc33ac"}, + {1100000, "cd87368b14488a85c2a1064ccf61855db4664919c8c2ddef1c6f088951700f19"}, + {1125000, "04c19d19ed31300a088b9485ddea0a2738821a7ea7da42f8324c8144f4e8c9c3"} }; } // namespace cn diff --git a/src/CryptoNoteCore/BlockIndex.cpp b/src/CryptoNoteCore/BlockIndex.cpp index 243170f26..48a869336 100644 --- a/src/CryptoNoteCore/BlockIndex.cpp +++ b/src/CryptoNoteCore/BlockIndex.cpp @@ -34,6 +34,12 @@ namespace cn { return result; } + crypto::Hash BlockIndex::getHashOfIds(uint32_t startBlockIndex, uint32_t maxCount) const { + /* This can be made more efficient by hashing in-place instead of copying all hashes, but it needs three function hash code */ + std::vector block_ids = getBlockIds(startBlockIndex, maxCount); + return crypto::cn_fast_hash(block_ids.data(), block_ids.size() * sizeof(crypto::Hash)); + } + bool BlockIndex::findSupplement(const std::vector& ids, uint32_t& offset) const { for (const auto& id : ids) { if (getBlockHeight(id, offset)) { diff --git a/src/CryptoNoteCore/BlockIndex.h b/src/CryptoNoteCore/BlockIndex.h index c122c315e..d64b1a9ef 100644 --- a/src/CryptoNoteCore/BlockIndex.h +++ b/src/CryptoNoteCore/BlockIndex.h @@ -58,6 +58,7 @@ namespace cn crypto::Hash getBlockId(uint32_t height) const; std::vector getBlockIds(uint32_t startBlockIndex, uint32_t maxCount) const; + crypto::Hash getHashOfIds(uint32_t startBlockIndex, uint32_t maxCount) const; bool findSupplement(const std::vector& ids, uint32_t& offset) const; std::vector buildSparseChain(const crypto::Hash& startBlockId) const; crypto::Hash getTailId() const; diff --git a/src/CryptoNoteCore/Blockchain.cpp b/src/CryptoNoteCore/Blockchain.cpp index ca0223c6c..61e370af1 100644 --- a/src/CryptoNoteCore/Blockchain.cpp +++ b/src/CryptoNoteCore/Blockchain.cpp @@ -1,7 +1,7 @@ // Copyright (c) 2011-2017 The Cryptonote developers // Copyright (c) 2017-2018 The Circle Foundation & Conceal Devs // Copyright (c) 2018-2020 Karbo developers -// Copyright (c) 2018-2025 Conceal Network & Conceal Devs +// Copyright (c) 2018-2026 Conceal Network & Conceal Devs // // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -367,7 +368,6 @@ namespace cn m_blockchainIndexesEnabled(blockchainIndexesEnabled), m_blockchainAutosaveEnabled(blockchainAutosaveEnabled), logger(logger, "Blockchain") - { } @@ -475,7 +475,6 @@ namespace cn try { m_testnet = testnet; - m_checkpoints.set_testnet(testnet); std::lock_guard lk(m_blockchain_lock); if (!config_folder.empty() && !tools::create_directories_if_necessary(config_folder)) @@ -486,6 +485,13 @@ namespace cn m_config_folder = config_folder; + m_checkpoints.init_targets(testnet, appendPath(config_folder, m_currency.checkpointFileName())); + // Only load checkpoint.dat if it is the new chunk-based format. + // Old-style files (first entry = genesis block hash) are ignored here; + // they are handled once in the try block below after the blockchain is loaded. + if (!m_checkpoints.is_old_style_checkpoint_file()) + m_checkpoints.load_checkpoints_from_file(); + if (!m_blocks.open(appendPath(config_folder, m_currency.blocksFileName()), appendPath(config_folder, m_currency.blockIndexesFileName()), 1024)) { @@ -516,6 +522,12 @@ namespace cn logger(ERROR, BRIGHT_RED) << "Failed to rebuild cache"; return false; } + + if (!storeCache()) + { + logger(ERROR, BRIGHT_RED) << "Failed to save rebuilt blockchain cache"; + return false; + } } } @@ -551,6 +563,12 @@ namespace cn logger(ERROR, BRIGHT_RED) << "Failed to rebuild cache"; return false; } + + if (!storeCache()) + { + logger(ERROR, BRIGHT_RED) << "Failed to save rebuilt blockchain cache"; + return false; + } } catch (const std::exception&) { @@ -597,22 +615,498 @@ namespace cn } } + // Convert old-style checkpoints (individual block hashes) to new-style (list hashes) + // ONLY if we don't have valid chunks covering the hardcoded checkpoint range. + // If chunks exist and cover the hardcoded checkpoints, we're using the new chunked system + // and don't need to convert targets (they're only needed for old-style full checkpoint lists). try { - /* If the current checkpoint is invalid, then rollback the chain to the last - valid checkpoint and try again. */ - uint32_t lastValidCheckpointHeight = 0; - if (!checkCheckpoints(lastValidCheckpointHeight)) + uint32_t currentHeight = static_cast(m_blocks.size() - 1); + uint32_t greatestTargetHeight = m_checkpoints.get_greatest_target_height(); + uint32_t currentCoveredHeight = m_checkpoints.get_covered_height(); + uint32_t computeUpTo = std::min(greatestTargetHeight, currentHeight); + + auto getBlockIdsFunc = [this](uint32_t startHeight, uint32_t maxCount) -> std::vector { + return m_blockIndex.getBlockIds(startHeight, maxCount); + }; + + if (currentHeight > 0 && greatestTargetHeight > 0 && computeUpTo > 0) { - logger(WARNING, BRIGHT_YELLOW) << "Invalid checkpoint. Rollback blockchain to last valid checkpoint at height " - << lastValidCheckpointHeight; - rollbackBlockchainTo(lastValidCheckpointHeight); + uint32_t chunk_size = m_checkpoints.get_chunk_size(); + uint32_t last_chunk = (computeUpTo - 1) / chunk_size; + + if (m_checkpoints.is_old_style_checkpoint_file()) + { + // ── ONE-TIME TRANSITION ────────────────────────────────────────────── + // Convert m_targets to cumulative list hashes up to what we actually have, + // then rebuild all chunks from scratch and write the new-format file. + // Writing chunk 0 overwrites the old file — transition never repeats. + logger(INFO) << "Old-style checkpoint.dat: one-time transition to chunk format " + << "(up to height " << computeUpTo << ")"; + + m_checkpoints.convert_old_checkpoints_to_list_hashes(getBlockIdsFunc, computeUpTo); + + for (uint32_t idx = 0; idx <= last_chunk; ++idx) + { + uint32_t chunk_start = idx * chunk_size + 1; + uint32_t chunk_end = (idx + 1) * chunk_size; + if (currentHeight < chunk_end) break; + if (!m_checkpoints.add_chunk_from_block_ids(getBlockIdsFunc, chunk_start)) + { logger(WARNING) << "Transition: chunk " << idx << " failed validation — stopping"; break; } + if (!m_checkpoints.add_verified_chunk_to_file(idx)) + logger(WARNING) << "Transition: chunk " << idx << " could not be written"; + } + + logger(INFO) << "Transition complete — checkpoint.dat is now chunk-based"; + } + else if (currentCoveredHeight < computeUpTo) + { + // ── NORMAL PATH ────────────────────────────────────────────────────── + // New-style or absent: append only the missing chunks up to computeUpTo. + uint32_t first_chunk = m_checkpoints.get_chunk_count(); + + logger(INFO) << "Appending checkpoint chunks " << first_chunk << ".." << last_chunk + << " (heights " << (currentCoveredHeight + 1) << ".." << computeUpTo << ")"; + + for (uint32_t idx = first_chunk; idx <= last_chunk; ++idx) + { + uint32_t chunk_start = idx * chunk_size + 1; + uint32_t chunk_end = (idx + 1) * chunk_size; + if (currentHeight < chunk_end) + { logger(DEBUGGING) << "Chunk " << idx << " incomplete — will finish during sync"; break; } + if (!m_checkpoints.add_chunk_from_block_ids(getBlockIdsFunc, chunk_start)) + { logger(WARNING) << "Chunk " << idx << " failed validation — stopping"; break; } + if (!m_checkpoints.add_verified_chunk_to_file(idx)) + logger(WARNING) << "Chunk " << idx << " could not be appended to checkpoint.dat"; + } + } } } - catch (const std::exception&) + catch (const std::exception& e) { - logger(ERROR, BRIGHT_RED) << "Error checking/rolling back checkpoints"; + logger(WARNING, BRIGHT_YELLOW) << "Error building checkpoint chunks: " << e.what(); + } + + // Generate checkpoint.dat from local blockchain if it doesn't exist or is incomplete + // + // CHUNKED CHECKPOINT GENERATION: + // Instead of storing all block hashes (60MB), we now store chunk hashes (~6KB): + // - Each chunk = hash of 10,000 consecutive blocks + // - For 1.87M blocks: 187 chunks × 32 bytes = ~6KB + // - Benefits: Fast P2P comparison, efficient disagreement detection, small file size + try + { + uint32_t currentHeight = static_cast(m_blocks.size() - 1); + uint32_t greatestTargetHeight = m_checkpoints.get_greatest_target_height(); + uint32_t currentCoveredHeight = m_checkpoints.get_covered_height(); + + // If we have a blockchain and checkpoint targets, but no checkpoint chunks (or incomplete), + // generate them from the local blockchain + if (currentHeight > 0 && greatestTargetHeight > 0) + { + // Calculate target height: up to greatest known target, but not more than current height + uint32_t targetHeight = std::min(greatestTargetHeight, currentHeight); + + if (currentCoveredHeight < targetHeight) + { + uint32_t existing_chunk_count = m_checkpoints.get_chunk_count(); + + // If we already have some chunks, only create the missing ones incrementally + // Otherwise, generate all chunks from scratch + if (existing_chunk_count > 0) + { + logger(INFO) << "Creating missing checkpoint chunk(s) from " << (currentCoveredHeight + 1) + << " to " << greatestTargetHeight << " (last hardcoded checkpoint)"; + + uint32_t chunk_size = m_checkpoints.get_chunk_size(); + auto getBlockIdsFunc = [this](uint32_t startHeight, uint32_t maxCount) -> std::vector { + return m_blockIndex.getBlockIds(startHeight, maxCount); + }; + + // Calculate which chunks we need to create + // Find the last chunk we have + uint32_t last_chunk_index; + if (currentCoveredHeight <= chunk_size) + { + last_chunk_index = 0; // We have at least chunk 0 + } + else + { + last_chunk_index = (currentCoveredHeight - 1) / chunk_size; + } + + // Find which chunk the last hardcoded checkpoint belongs to + uint32_t target_chunk_index; + if (greatestTargetHeight <= chunk_size) + { + target_chunk_index = 0; + } + else + { + target_chunk_index = (greatestTargetHeight - 1) / chunk_size; + } + + // Create missing chunks one by one + for (uint32_t chunk_idx = last_chunk_index + 1; chunk_idx <= target_chunk_index; chunk_idx++) + { + // SIMPLIFIED: All chunks are uniform (block 0 excluded) + uint32_t chunk_start_height = chunk_idx * chunk_size + 1; + uint32_t chunk_end_height = (chunk_idx + 1) * chunk_size; + + if (currentHeight >= chunk_end_height) + { + if (m_checkpoints.add_chunk_from_block_ids(getBlockIdsFunc, chunk_start_height)) + { + // Auto-confirm chunks up to the last hardcoded checkpoint + // These chunks are validated against hardcoded checkpoints in add_chunk_from_block_ids, + // so they're safe to add to checkpoint.dat immediately + if (m_checkpoints.add_verified_chunk_to_file(chunk_idx)) + { + logger(INFO) << "Created and saved checkpoint chunk " << chunk_idx + << " (blocks " << chunk_start_height << "-" << chunk_end_height + << ", validated against hardcoded checkpoints)"; + } + else + { + logger(WARNING) << "Created chunk " << chunk_idx << " but failed to save to checkpoint.dat"; + } + } + else + { + logger(WARNING) << "Failed to create chunk " << chunk_idx << " - blockchain mismatch detected"; + break; // Stop if we can't create a chunk + } + } + else + { + logger(INFO) << "Skipping chunk " << chunk_idx + << " - not enough blocks (have " << currentHeight + << ", need " << chunk_end_height << ")"; + break; // Can't create more chunks if we don't have enough blocks + } + } + } + else + { + // REFINED STRATEGY: Initial setup (no checkpoint.dat file) + // 1. Generate chunks up to last CryptoNoteConfig.h checkpoint (PRIORITY 1) + // 2. If DNS checkpoints exist beyond that, try to extend up to DNS checkpoint (PRIORITY 2) + // 3. Beyond that, chunks will be validated via P2P (autonomous) + + logger(INFO, BRIGHT_GREEN) << "Initial setup: Generating checkpoint chunks from local blockchain (height " + << currentHeight << ", target up to " << greatestTargetHeight << " from CryptoNoteConfig.h)"; + + // Step 1: Generate chunks up to last CryptoNoteConfig.h checkpoint + auto getBlockIdsFunc = [this](uint32_t startHeight, uint32_t maxCount) -> std::vector { + return m_blockIndex.getBlockIds(startHeight, maxCount); + }; + + if (m_checkpoints.generate_chunks_from_block_ids(getBlockIdsFunc, targetHeight)) + { + uint32_t chunkCount = m_checkpoints.get_chunk_count(); + uint32_t coveredHeight = m_checkpoints.get_covered_height(); + logger(INFO, BRIGHT_GREEN) << "Successfully generated " << chunkCount + << " checkpoint chunks from CryptoNoteConfig.h (covers up to height " << coveredHeight << ")"; + + // Step 2: Create chunks beyond hardcoded checkpoints up to current blockchain height + // These chunks will be stored in memory and validated via P2P consensus + if (currentHeight > greatestTargetHeight) + { + uint32_t chunk_size = m_checkpoints.get_chunk_size(); + uint32_t last_hardcoded_chunk_index = (greatestTargetHeight - 1) / chunk_size; + uint32_t current_chunk_index = (currentHeight - 1) / chunk_size; + + logger(INFO) << "Creating chunks " << (last_hardcoded_chunk_index + 1) + << " to " << current_chunk_index + << " (beyond hardcoded checkpoints, up to current height " << currentHeight << ")"; + + // Create chunks beyond hardcoded checkpoints (these need P2P validation) + // Use same logic as existing code path (lines 915-952) for consistency + for (uint32_t chunk_idx = last_hardcoded_chunk_index + 1; chunk_idx <= current_chunk_index; chunk_idx++) + { + // SIMPLIFIED: All chunks are uniform (block 0 excluded) + uint32_t chunk_start_height = chunk_idx * chunk_size + 1; + uint32_t chunk_end_height = (chunk_idx + 1) * chunk_size; + + // Only create if we have all blocks for this chunk + if (currentHeight >= chunk_end_height) + { + // Check if chunk already exists in memory (from previous session) + crypto::Hash existing_chunk_hash = m_checkpoints.get_chunk_hash(chunk_idx); + if (existing_chunk_hash != NULL_HASH) + { + logger(DEBUGGING) << "Chunk " << chunk_idx << " already exists in memory (from previous session), awaiting P2P validation"; + continue; // Skip to next chunk + } + + // Create new chunk + if (!m_checkpoints.add_chunk_from_block_ids(getBlockIdsFunc, chunk_start_height)) + { + logger(WARNING) << "Failed to create chunk " << chunk_idx << " - blockchain mismatch detected"; + break; + } + // Chunk created and hash computed - logging is done in CheckpointsList.cpp + // NOTE: We do NOT call add_verified_chunk_to_file() here + // These chunks need peer consensus before being saved to checkpoint.dat + } + else + { + logger(INFO) << "Skipping chunk " << chunk_idx + << " - not enough blocks (have " << currentHeight + << ", need " << chunk_end_height << ")"; + break; + } + } + } + + logger(INFO) << "Initial setup complete. Chunks beyond CryptoNoteConfig.h checkpoints will be validated via P2P consensus."; + } + else + { + logger(WARNING, BRIGHT_YELLOW) << "Checkpoint chunk generation failed. " + << "This may happen if the blockchain doesn't match expected checkpoints from CryptoNoteConfig.h."; + } + } + } + else + { + logger(INFO, BRIGHT_GREEN) << "Checkpoint chunks already exist (covers up to height " + << currentCoveredHeight << ")"; + + // CRITICAL: Validate chunks against checkpoints from CryptoNoteConfig.h + // This detects if maintainer added new checkpoints that don't match our checkpoint.dat + // If validation fails, rollback blockchain and truncate checkpoint.dat + auto getBlockIdsFunc = [this](uint32_t startHeight, uint32_t maxCount) -> std::vector { + return m_blockIndex.getBlockIds(startHeight, maxCount); + }; + + // Get current blockchain height for validation + uint32_t current_height = static_cast(m_blocks.size() > 0 ? m_blocks.size() - 1 : 0); + auto validation_result = m_checkpoints.validate_chunks_against_checkpoints(getBlockIdsFunc, current_height); + + if (!validation_result.is_valid) + { + uint32_t mismatched_chunk = validation_result.first_mismatched_chunk_index; + uint32_t rollback_height = mismatched_chunk * m_checkpoints.get_chunk_size(); + + logger(ERROR, BRIGHT_RED) << "CHECKPOINT VALIDATION FAILED: " + << "Chunk " << mismatched_chunk << " (contains checkpoint from CryptoNoteConfig.h) " + << "does not match checkpoint.dat. " + << "This indicates maintainer added a new checkpoint or blockchain diverged. " + << "Rolling back blockchain to height " << rollback_height + << " and truncating checkpoint.dat to chunk " << (mismatched_chunk > 0 ? mismatched_chunk - 1 : 0); + + // Truncate checkpoint.dat to last valid chunk (before the mismatch) + uint32_t last_valid_chunk = (mismatched_chunk > 0) ? (mismatched_chunk - 1) : 0; + if (!m_checkpoints.truncate_checkpoint_file(last_valid_chunk)) + { + logger(ERROR, BRIGHT_RED) << "Failed to truncate checkpoint.dat - file may be corrupted"; + } + + // Rollback blockchain to the chunk boundary before the mismatch + if (rollback_height > 0 && rollback_height < currentHeight) + { + logger(INFO, BRIGHT_YELLOW) << "Rolling back blockchain from height " << currentHeight + << " to height " << rollback_height; + if (!rollbackBlockchainTo(rollback_height)) + { + logger(ERROR, BRIGHT_RED) << "Failed to rollback blockchain - node may be in inconsistent state"; return false; + } + logger(INFO, BRIGHT_GREEN) << "Successfully rolled back blockchain to height " << rollback_height; + } + else + { + logger(ERROR, BRIGHT_RED) << "Cannot rollback to height " << rollback_height + << " (current height: " << currentHeight << ")"; + return false; + } + } + else + { + // VERSION 2 CHECKPOINT SYSTEM: Create missing chunks beyond last hardcoded checkpoint + // Only for P2P version 2+ nodes (chunked checkpoint system) + // Version 1 nodes stick with old checkpoint method only + if (cn::P2P_CURRENT_VERSION >= cn::P2P_CHECKPOINT_LIST_VERSION) + { + // Check if we're past the last chunk boundary and need to create new chunks + // These chunks are beyond the last hardcoded checkpoint, so they need P2P validation + uint32_t chunk_size = m_checkpoints.get_chunk_size(); + uint32_t last_chunk_index; + + if (currentCoveredHeight <= chunk_size) + { + last_chunk_index = 0; + } + else + { + last_chunk_index = (currentCoveredHeight - 1) / chunk_size; + } + + // Calculate which chunk the current height belongs to + uint32_t current_chunk_index; + if (currentHeight <= chunk_size) + { + current_chunk_index = 0; + } + else + { + current_chunk_index = (currentHeight - 1) / chunk_size; + } + + // If we're past the last chunk boundary, create missing chunks + if (current_chunk_index > last_chunk_index && currentHeight > greatestTargetHeight) + { + uint32_t num_chunks_to_create = current_chunk_index - last_chunk_index; + if (num_chunks_to_create == 1) { + logger(INFO) << "Checking for missing checkpoint chunk " << current_chunk_index + << " beyond last hardcoded checkpoint (heights " + << (currentCoveredHeight + 1) << " to " << currentHeight << ")"; + } else { + logger(INFO) << "Checking for missing checkpoint chunks beyond last hardcoded checkpoint (from chunk " + << (last_chunk_index + 1) << " to " << current_chunk_index + << ", heights " << (currentCoveredHeight + 1) << " to " << currentHeight << ")"; + } + + auto getBlockIdsFunc = [this](uint32_t startHeight, uint32_t maxCount) -> std::vector { + return m_blockIndex.getBlockIds(startHeight, maxCount); + }; + + // Create missing chunks one by one (these need P2P validation, not auto-confirmed) + for (uint32_t chunk_idx = last_chunk_index + 1; chunk_idx <= current_chunk_index; chunk_idx++) + { + // SIMPLIFIED: All chunks are uniform (block 0 excluded) + uint32_t chunk_start_height = chunk_idx * chunk_size + 1; + uint32_t chunk_end_height = (chunk_idx + 1) * chunk_size; + + // Only create if we have all blocks for this chunk + if (currentHeight >= chunk_end_height) + { + // Check if chunk already exists in memory (from previous session) + crypto::Hash existing_chunk_hash = m_checkpoints.get_chunk_hash(chunk_idx); + if (existing_chunk_hash != NULL_HASH) + { + logger(DEBUGGING) << "Chunk " << chunk_idx << " already exists in memory (from previous session), awaiting P2P validation"; + continue; // Skip to next chunk + } + + // Create new chunk + if (!m_checkpoints.add_chunk_from_block_ids(getBlockIdsFunc, chunk_start_height)) + { + logger(WARNING) << "Failed to create chunk " << chunk_idx << " - blockchain mismatch detected"; + break; + } + // Chunk created and hash computed - logging is done in CheckpointsList.cpp + // NOTE: We do NOT call add_verified_chunk_to_file() here + // These chunks need peer consensus before being saved to checkpoint.dat + } + else + { + logger(INFO) << "Skipping chunk " << chunk_idx + << " - not enough blocks (have " << currentHeight + << ", need " << chunk_end_height << ")"; + break; + } + } + + // Log unverified chunks that need P2P validation + std::vector unverified = m_checkpoints.get_unverified_chunks(); + if (!unverified.empty()) + { + if (unverified.size() == 1) + logger(INFO) << "Found 1 unverified chunk: " << unverified[0]; + else + logger(INFO) << "Found " << unverified.size() << " unverified chunks from " << unverified[0]; + } + } + } + + // Check if we're missing chunks up to the last hardcoded checkpoint + // If we're synced past the last checkpoint, we can safely create missing chunks + // Only for version 2+ nodes (version 1 uses old checkpoint method) + if (cn::P2P_CURRENT_VERSION >= cn::P2P_CHECKPOINT_LIST_VERSION && + currentCoveredHeight < greatestTargetHeight && currentHeight >= greatestTargetHeight) + { + logger(INFO) << "Creating missing checkpoint chunk(s) from " << (currentCoveredHeight + 1) + << " to " << greatestTargetHeight << " (last hardcoded checkpoint)"; + + uint32_t chunk_size = m_checkpoints.get_chunk_size(); + + // Calculate which chunks we need to create + // Find the last chunk we have + uint32_t last_chunk_index; + if (currentCoveredHeight <= chunk_size) + { + last_chunk_index = 0; // We have at least chunk 0 + } + else + { + last_chunk_index = (currentCoveredHeight - 1) / chunk_size; + } + + // Find which chunk the last hardcoded checkpoint belongs to + uint32_t target_chunk_index; + if (greatestTargetHeight <= chunk_size) + { + target_chunk_index = 0; + } + else + { + target_chunk_index = (greatestTargetHeight - 1) / chunk_size; + } + + // Create missing chunks one by one + for (uint32_t chunk_idx = last_chunk_index + 1; chunk_idx <= target_chunk_index; chunk_idx++) + { + // SIMPLIFIED: All chunks are uniform (block 0 excluded) + uint32_t chunk_start_height = chunk_idx * chunk_size + 1; + uint32_t chunk_end_height = (chunk_idx + 1) * chunk_size; + + if (currentHeight >= chunk_end_height) + { + logger(INFO, BRIGHT_GREEN) << "Creating missing chunk " << chunk_idx + << " (blocks " << chunk_start_height << " to " << chunk_end_height << ")"; + + if (m_checkpoints.add_chunk_from_block_ids(getBlockIdsFunc, chunk_start_height)) + { + // Auto-confirm chunks up to the last hardcoded checkpoint + // These chunks are validated against hardcoded checkpoints in add_chunk_from_block_ids, + // so they're safe to add to checkpoint.dat immediately + if (m_checkpoints.add_verified_chunk_to_file(chunk_idx)) + { + logger(INFO, BRIGHT_GREEN) << "Successfully created and saved chunk " << chunk_idx + << " to checkpoint.dat (validated against hardcoded checkpoints)"; + } + else + { + logger(WARNING, BRIGHT_YELLOW) << "Created chunk " << chunk_idx + << " but failed to save to checkpoint.dat"; + } + } + else + { + logger(WARNING, BRIGHT_YELLOW) << "Failed to create chunk " << chunk_idx + << " - this may indicate a blockchain mismatch"; + break; // Stop if we can't create a chunk + } + } + else + { + logger(INFO) << "Skipping chunk " << chunk_idx + << " - not enough blocks yet (have " << currentHeight + << ", need " << chunk_end_height << ")"; + break; // Can't create more chunks if we don't have enough blocks + } + } + } + } + } + } + } + catch (const std::exception& e) + { + logger(WARNING, BRIGHT_YELLOW) << "Error generating checkpoint list: " << e.what(); + // Don't fail initialization - checkpoint generation is optional for lone wolf nodes } // Initialize upgrade detectors with proper exception handling @@ -656,25 +1150,75 @@ namespace cn bool Blockchain::checkCheckpoints(uint32_t &lastValidCheckpointHeight) { - std::vector checkpointHeights = m_checkpoints.getCheckpointHeights(); - for (const auto &checkpointHeight : checkpointHeights) + // Get checkpoint targets from hardcoded checkpoints in CryptoNoteConfig.h + // These are the expected hashes of block ID lists at specific heights + std::vector> checkpointHeights = m_checkpoints.get_checkpoint_targets(); + lastValidCheckpointHeight = 0; + + // If no checkpoints are configured, validation passes + if (checkpointHeights.empty()) { - if (m_blocks.size() <= checkpointHeight) - { - return true; - } + return true; + } - if (m_checkpoints.check_block(checkpointHeight, getBlockIdByHeight(checkpointHeight))) + uint32_t currentHeight = static_cast(m_blocks.size() - 1); + + /* Checkpoints are in reverse order (highest first), so we check from highest to lowest. + * If the highest checkpoint fails, we check the previous one, and so on, + * until we find a valid checkpoint. Once we find a valid one, we rollback to that height. */ + bool foundValidCheckpoint = false; + for (const auto &check : checkpointHeights) + { + uint32_t height = check.first; + + // Only validate checkpoints that are at or below the current blockchain height + if (currentHeight < height) + continue; + + // Compute hash of block IDs from genesis (height 0) to this checkpoint height + // Compare against the hardcoded checkpoint target from CryptoNoteConfig.h + crypto::Hash hv = m_blockIndex.getHashOfIds(0, height+1); + if (hv == check.second) { - lastValidCheckpointHeight = checkpointHeight; + lastValidCheckpointHeight = height; + foundValidCheckpoint = true; + logger(INFO, BRIGHT_GREEN) << "Found valid checkpoint at height " << height; + break; } else { - return false; + logger(WARNING, BRIGHT_YELLOW) << "Checkpoint validation failed for height " << height << + " - computed hash: " << hv << " expected (from hardcoded checkpoints): " << check.second; + // Continue checking previous checkpoints } } - logger(INFO, BRIGHT_WHITE) << "Checkpoints passed"; - return true; + + if(foundValidCheckpoint) + { + logger(INFO, BRIGHT_WHITE) << "Checkpoints validated, last valid checkpoint at height " << lastValidCheckpointHeight; + uint32_t n_valid_blocks = lastValidCheckpointHeight+1; + if(m_checkpoints.get_points_size() < n_valid_blocks) + m_checkpoints.set_checkpoint_list(m_blockIndex.getBlockIds(0, n_valid_blocks)); + + // If we found a valid checkpoint but it's not at the current height, we need to rollback + if (lastValidCheckpointHeight < currentHeight) + { + logger(WARNING, BRIGHT_YELLOW) << "Blockchain height " << currentHeight << + " is beyond last valid checkpoint " << lastValidCheckpointHeight << ", rollback required"; + return false; // Signal that rollback is needed + } + + return true; // All checkpoints up to current height are valid + } + else + { + // No valid checkpoint found - this is a problem + logger(ERROR, BRIGHT_RED) << "No valid checkpoint found! All checkpoint validations failed."; + // Don't set lastValidCheckpointHeight to 0 - that would rollback the entire chain + // Instead, return true to allow the blockchain to continue (maybe checkpoints need updating) + // But log this as a serious error + return true; // Allow continuation, but this should be investigated + } } bool Blockchain::rebuildCache() @@ -690,6 +1234,7 @@ namespace cn m_spent_keys.clear(); m_outputs.clear(); m_multisignatureOutputs.clear(); + crypto::Hash prevBlockHash = NULL_HASH; for (uint32_t b = 0; b < m_blocks.size(); ++b) { if (b % 1000 == 0) @@ -699,6 +1244,20 @@ namespace cn const BlockEntry &block = m_blocks[b]; crypto::Hash blockHash = get_block_hash(block.bl); + + // Verify chain linkage: every block except genesis must reference the previous block's hash. + // A mismatch means blockchain.dat was tampered or is corrupt at this height. + if (b > 0 && block.bl.previousBlockHash != prevBlockHash) + { + logger(ERROR, BRIGHT_RED) + << "Chain linkage broken at height " << b + << ": block.previousBlockHash=" << block.bl.previousBlockHash + << " expected=" << prevBlockHash + << ". blockchain.dat is corrupt or has been tampered."; + return false; + } + prevBlockHash = blockHash; + m_blockIndex.push(blockHash); uint64_t interest = 0; for (uint32_t t = 0; t < block.transactions.size(); ++t) @@ -838,13 +1397,15 @@ namespace cn BlockCacheSerializer ser(*this, getTailId(), logger.getLogger()); const std::string &blocksCacheFileName = m_currency.blocksCacheFileName(); - std::string blockCacheBkpFileName = blocksCacheFileName + ".bkp"; + const std::string blocksCachePath = appendPath(m_config_folder, blocksCacheFileName); + const std::string blockCacheBkpPath = blocksCachePath + ".bkp"; try { - std::rename(blocksCacheFileName.c_str(), blockCacheBkpFileName.c_str()); // fail here can be ignored + std::remove(blockCacheBkpPath.c_str()); // fail here can be ignored + std::rename(blocksCachePath.c_str(), blockCacheBkpPath.c_str()); // fail here can be ignored - if (!ser.save(appendPath(m_config_folder, blocksCacheFileName))) + if (!ser.save(blocksCachePath)) { logger(ERROR, BRIGHT_RED) << "Failed to save blockchain cache"; return false; @@ -1548,6 +2109,46 @@ namespace cn return true; } + bool Blockchain::is_alternative_block_allowed(uint32_t blockchain_height, uint32_t block_height) const { + if (0 == block_height) + return false; + + uint32_t lowest_height = blockchain_height - cn::parameters::CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW; + + if (blockchain_height < cn::parameters::CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW) + { + lowest_height = 0; + } + + if (block_height < lowest_height && !m_checkpoints.is_in_checkpoint_zone(block_height)) + { + logger(logging::DEBUGGING, logging::WHITE) + << "<< Checkpoints.cpp << " + << "Reorganization depth too deep : " << (blockchain_height - block_height) << ". Block Rejected"; + return false; + } + + // Prefer chunk-based coverage (live chain): only blocks beyond the confirmed + // checkpoint zone are allowed as alternative heads. + uint32_t covered_height = m_checkpoints.get_covered_height(); + if (covered_height > 0) { + return covered_height < block_height; + } + + // No chunks present (e.g. unit tests, early bootstrap): fall back to + // target-based logic. Find the highest checkpoint target whose height has + // already been reached by the current blockchain. Blocks at or below that + // height cannot be alternative heads; blocks above it can. + uint32_t checkpoint_zone_height = 0; + for (const auto& target : m_checkpoints.get_checkpoint_targets()) { + uint32_t target_height = target.first; + if (target_height <= blockchain_height && target_height > checkpoint_zone_height) { + checkpoint_zone_height = target_height; + } + } + return checkpoint_zone_height < block_height; + } + bool Blockchain::handle_alternative_block(const Block &b, const crypto::Hash &id, block_verification_context &bvc, bool sendNewAlternativeBlockMessage) { try @@ -1563,10 +2164,7 @@ namespace cn return false; } - /* in the absence of a better solution, we fetch checkpoints from dns records */ - m_checkpoints.load_checkpoints_from_dns(); - - if (!m_checkpoints.is_alternative_block_allowed(getCurrentBlockchainHeight(), block_height)) + if (!is_alternative_block_allowed(getCurrentBlockchainHeight(), block_height)) { logger(DEBUGGING) << "Block with id: " << id << std::endl << " can't be accepted for alternative chain, block height: " << block_height << std::endl @@ -1661,8 +2259,8 @@ namespace cn bei.bl = b; bei.height = alt_chain.size() ? it_prev->second.height + 1 : mainPrevHeight + 1; - bool is_a_checkpoint; - if (!m_checkpoints.check_block(bei.height, id, is_a_checkpoint)) + auto checkpoint_status = m_checkpoints.check_checkpoint(bei.height, id); + if ( checkpoint_status == CheckpointList::is_in_zone_failed ) { logger(ERROR, BRIGHT_RED) << "Checkpoint validaton failure"; @@ -1728,7 +2326,7 @@ namespace cn alt_chain.push_back(i_res.first->first); - if (is_a_checkpoint) + if ( checkpoint_status == CheckpointList::is_checkpointed ) { //do reorganize! logger(INFO, BRIGHT_GREEN) << "###### REORGANIZE on height: " << m_alternative_chains[alt_chain.front()].height << " of " << m_blocks.size() - 1 << ", checkpoint is found in alternative chain on height " << bei.height; @@ -2660,15 +3258,13 @@ namespace cn auto longhashTimeStart = std::chrono::steady_clock::now(); crypto::Hash proof_of_work = NULL_HASH; - if (m_checkpoints.is_in_checkpoint_zone(getCurrentBlockchainHeight())) + auto checkpoint_status = m_checkpoints.check_checkpoint(getCurrentBlockchainHeight(), blockHash); + if (checkpoint_status == CheckpointList::is_in_zone_failed ) { - if (!m_checkpoints.check_block(getCurrentBlockchainHeight(), blockHash)) - { - bvc.m_verification_failed = true; - return false; - } + bvc.m_verification_failed = true; + return false; } - else + else if(checkpoint_status == CheckpointList::is_out_of_zone ) { if (!m_currency.checkProofOfWork(m_cn_context, blockData, currentDifficulty, proof_of_work)) { @@ -2788,6 +3384,74 @@ namespace cn bvc.m_added_to_main_chain = true; + // Check if we've reached a chunk boundary and need to create a new chunk hash + // This happens every chunk_size blocks (10,000 for mainnet, 25,000 for testnet) + // SIMPLIFIED: Block 0 (genesis) is excluded from chunks, all chunks are uniform + uint32_t current_height = block.height; + uint32_t chunk_size = m_checkpoints.get_chunk_size(); + uint32_t current_covered_height = m_checkpoints.get_covered_height(); + + // Skip block 0 (genesis) - it's validated separately and not part of any chunk + if (current_height > 0 && current_height > current_covered_height) + { + // SIMPLIFIED: Calculate which chunk this height belongs to + // All chunks are uniform: chunk_size blocks each + // chunk[0]: blocks 1 to chunk_size + // chunk[1]: blocks (chunk_size + 1) to (2 * chunk_size) + // chunk[n]: blocks (n * chunk_size + 1) to ((n + 1) * chunk_size) + uint32_t chunk_index = (current_height - 1) / chunk_size; + uint32_t chunk_start_height = chunk_index * chunk_size + 1; + uint32_t chunk_end_height = (chunk_index + 1) * chunk_size; + + // If we've reached the end of a chunk, create the chunk hash + // Only create if we haven't already created this chunk + if (current_height == chunk_end_height && current_height > current_covered_height) + { + // Check if chunk already exists (might have been created during initial generation + // or as a prefetched placeholder). A prefetched chunk must be verified here. + bool chunk_is_prefetched = m_checkpoints.is_chunk_prefetched(chunk_index); + crypto::Hash existing_chunk_hash = m_checkpoints.get_chunk_hash(chunk_index); + + if (existing_chunk_hash == NULL_HASH || chunk_is_prefetched) + { + logger(INFO, BRIGHT_GREEN) << "Reached chunk boundary at height " << current_height + << " (chunk " << chunk_index << ", blocks " << chunk_start_height + << " to " << chunk_end_height << "). " + << (chunk_is_prefetched ? "Verifying prefetched hash..." : "Computing chunk hash..."); + + auto getBlockIdsFunc = [this](uint32_t startHeight, uint32_t maxCount) -> std::vector { + return m_blockIndex.getBlockIds(startHeight, maxCount); + }; + + bool prefetch_mismatch = false; + if (!m_checkpoints.add_chunk_from_block_ids(getBlockIdsFunc, chunk_start_height, &prefetch_mismatch)) + { + if (prefetch_mismatch) + { + // Local chain diverges from the prefetched network consensus hash. + // The chunk has been re-queued as unverified; validate_unverified_chunks() + // will run M-of-K consensus and trigger a rollback if the network confirms + // the divergence. + logger(ERROR, BRIGHT_RED) + << "Chunk " << chunk_index << " PREFETCH MISMATCH at height " << current_height + << "! Local hash differs from prefetched network consensus. " + << "Consensus validation will trigger rollback if network confirms divergence."; + } + else + { + logger(WARNING, BRIGHT_YELLOW) << "Failed to compute chunk " << chunk_index + << " hash at height " << current_height; + } + } + } + else + { + logger(DEBUGGING) << "Chunk " << chunk_index << " already exists (hash: " << existing_chunk_hash + << "), skipping creation at height " << current_height; + } + } + } + m_upgradeDetectorV2.blockPushed(); m_upgradeDetectorV3.blockPushed(); m_upgradeDetectorV4.blockPushed(); @@ -3185,28 +3849,7 @@ namespace cn try { std::lock_guard lk(m_blockchain_lock); - - logger(INFO, BRIGHT_YELLOW) << "Rolling back blockchain to height " << height; - - // Check if we're already at or below the requested height - if (height >= m_blocks.size()) - { - logger(WARNING, BRIGHT_YELLOW) << "Requested rollback to height " << height - << " which is higher than current height " << m_blocks.size(); - return true; - } - - while (m_blocks.size() > height + 1) - { - if (!removeLastBlock()) - { - logger(ERROR, BRIGHT_RED) << "Failed to remove last block during rollback"; - return false; - } - } - - logger(INFO, BRIGHT_GREEN) << "Blockchain successfully rolled back to height: " << height << "Synchronization will resume"; - return true; + return rollback_to_height_impl(height); } catch (const std::exception&) { @@ -3215,6 +3858,38 @@ namespace cn } } + bool Blockchain::rollback_to_height_impl(uint32_t height) + { + logger(INFO, BRIGHT_YELLOW) << "Rolling back blockchain to height " << height; + + // Check if we're already at or below the requested height + if (height >= m_blocks.size()) + { + logger(WARNING, BRIGHT_YELLOW) << "Requested rollback to height " << height + << " which is higher than current height " << m_blocks.size(); + return true; + } + + // Validate height is reasonable (not 0, which would rollback genesis) + if (height == 0 && m_blocks.size() > 1) + { + logger(ERROR, BRIGHT_RED) << "Cannot rollback to height 0 (genesis block) - current height: " << m_blocks.size(); + return false; + } + + while (m_blocks.size() > height + 1) + { + if (!removeLastBlock()) + { + logger(ERROR, BRIGHT_RED) << "Failed to remove last block during rollback"; + return false; + } + } + + logger(INFO, BRIGHT_GREEN) << "Blockchain successfully rolled back to height: " << height << "Synchronization will resume"; + return true; + } + bool Blockchain::removeLastBlock() { if (m_blocks.empty()) @@ -3509,4 +4184,9 @@ namespace cn return m_checkpoints.is_in_checkpoint_zone(height); } + crypto::Hash Blockchain::getCheckpointHash(uint32_t height) const + { + std::lock_guard lk(m_blockchain_lock); + return m_blockIndex.getHashOfIds(0, height+1); + } } // namespace cn diff --git a/src/CryptoNoteCore/Blockchain.h b/src/CryptoNoteCore/Blockchain.h index 79506dcb5..fba03e42d 100644 --- a/src/CryptoNoteCore/Blockchain.h +++ b/src/CryptoNoteCore/Blockchain.h @@ -15,7 +15,7 @@ #include "Common/ObserverManager.h" #include "Common/Util.h" #include "CryptoNoteCore/BlockIndex.h" -#include "CryptoNoteCore/Checkpoints.h" +#include "CryptoNoteCore/CheckpointList.h" #include "CryptoNoteCore/Currency.h" #include "CryptoNoteCore/DepositIndex.h" #include "CryptoNoteCore/IBlockchainStorageObserver.h" @@ -68,7 +68,6 @@ namespace cn bool getLowerBound(uint64_t timestamp, uint64_t startOffset, uint32_t &height); std::vector getBlockIds(uint32_t startHeight, uint32_t maxCount); - void setCheckpoints(Checkpoints &&chk_pts) { m_checkpoints = std::move(chk_pts); } bool getBlocks(uint32_t start_offset, uint32_t count, std::list &blocks, std::list &txs); bool getBlocks(uint32_t start_offset, uint32_t count, std::list &blocks); bool getAlternativeBlocks(std::list &blocks); @@ -84,6 +83,10 @@ namespace cn bool haveTransaction(const crypto::Hash &id); bool haveTransactionKeyImagesAsSpent(const Transaction &tx); + CheckpointList& getCheckpointList() { + return m_checkpoints; + } + uint32_t getCurrentBlockchainHeight(); // TODO rename to getCurrentBlockchainSize crypto::Hash getTailId(); crypto::Hash getTailId(uint32_t &height); @@ -125,6 +128,8 @@ namespace cn uint64_t coinsEmittedAtHeight(uint64_t height); uint64_t difficultyAtHeight(uint64_t height); bool isInCheckpointZone(const uint32_t height) const; + crypto::Hash getCheckpointHash(uint32_t height) const; + bool is_alternative_block_allowed(uint32_t blockchain_height, uint32_t block_height) const; template bool scanOutputKeysForIndexes(const KeyInput &tx_in_to_key, visitor_t &vis, uint32_t *pmax_related_block_height = nullptr); @@ -220,6 +225,9 @@ namespace cn bool have_tx_keyimg_as_spent(const crypto::KeyImage &key_im); private: + // Private helper for rollback - performs the actual rollback operation + // Precondition: m_blockchain_lock must be held + bool rollback_to_height_impl(uint32_t height); bool m_testnet = false; struct MultisignatureOutputUsage { @@ -284,7 +292,7 @@ namespace cn outputs_container m_outputs; std::string m_config_folder; - Checkpoints m_checkpoints; + CheckpointList m_checkpoints; std::atomic m_is_in_checkpoint_zone; using Blocks = SwappedVector; @@ -482,4 +490,4 @@ namespace cn return true; } }; -} // namespace cn \ No newline at end of file +} // namespace cn diff --git a/src/CryptoNoteCore/CheckpointList.h b/src/CryptoNoteCore/CheckpointList.h new file mode 100644 index 000000000..2fa33b3ec --- /dev/null +++ b/src/CryptoNoteCore/CheckpointList.h @@ -0,0 +1,712 @@ +// Copyright (c) 2011-2017 The Cryptonote developers +// Copyright (c) 2017-2018 The Circle Foundation & Conceal Devs +// Copyright (c) 2018-2023 Conceal Network & Conceal Devs +// +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#pragma once +#include +#include +#include +#include + +#include "CryptoNoteBasicImpl.h" +#include +#include "../CryptoNoteConfig.h" // For P2P_CHECKPOINT_PEER_MIN_UPTIME_BLOCKS constants + +#ifdef _WIN32 +#undef ERROR +#endif +namespace cn +{ + class CheckpointList + { + public: + // Chunk size: number of blocks per checkpoint chunk + // Mainnet: 10,000 blocks per chunk (matches checkpoint frequency in CryptoNoteConfig.h) + // Testnet: 25,000 blocks per chunk (matches checkpoint frequency in CryptoNoteConfig.h) + static constexpr uint32_t CHUNK_SIZE_MAINNET = 10000; + static constexpr uint32_t CHUNK_SIZE_TESTNET = 25000; + + explicit CheckpointList(logging::ILogger& log) : logger(log, "checkpoint_list"), m_chunk_size(CHUNK_SIZE_MAINNET) {} + + void init_targets(bool is_testnet, const std::string& save_file); + + // Returns true if checkpoint.dat exists and its first entry is the genesis block hash, + // which identifies the old per-block format. New-style files start with a chunk hash + // (hash of blocks 1..chunk_size) which can never equal the genesis block hash. + bool is_old_style_checkpoint_file() const; + + // Convert old-style checkpoints (individual block hashes) to new-style (list hashes) + // This allows the same checkpoint data in CryptoNoteConfig.h to work with both systems + // getBlockIdsFunc: function that returns block IDs from genesis (0) to the specified height + // max_height: Only convert checkpoints up to this height (default: UINT32_MAX = convert all) + void convert_old_checkpoints_to_list_hashes( + std::function(uint32_t startHeight, uint32_t maxCount)> getBlockIdsFunc, + uint32_t max_height = UINT32_MAX); + + // Chunked checkpoint methods + // Generate checkpoint chunks from block IDs (used during initialization) + bool generate_chunks_from_block_ids( + std::function(uint32_t startHeight, uint32_t maxCount)> getBlockIdsFunc, + uint32_t max_height); + + // Add a single chunk (called when a new chunk_size blocks are added) + // This computes the hash for the latest chunk and stores it in memory only + // NOTE: Chunk is NOT saved to checkpoint.dat until peer consensus is reached + // After peer consensus, call add_verified_chunk_to_file() to save it + // + // out_prefetch_mismatch (optional): set to true when a prefetched consensus hash + // existed for this chunk but the locally computed hash disagreed. When this flag + // is set the chunk is stored with the local hash and queued for consensus validation + // (which will trigger a rollback if the network disagrees with the local chain). + bool add_chunk_from_block_ids( + std::function(uint32_t startHeight, uint32_t maxCount)> getBlockIdsFunc, + uint32_t chunk_start_height, + bool* out_prefetch_mismatch = nullptr); + + // --- Forward-prefetch API --- + // Add a chunk hash obtained from P2P consensus BEFORE the local chain has reached + // that chunk boundary. This extends the checkpoint zone so IBD can continue at + // fast speed. The hash is verified locally when add_chunk_from_block_ids() fires + // at the corresponding chunk boundary. + // Returns false if chunk already has a locally-computed (non-prefetched) hash. + bool add_prefetched_chunk(uint32_t chunk_index, const crypto::Hash& consensus_hash); + + // Returns true if the chunk at chunk_index was prefetched from the network and has + // not yet been confirmed by local computation. + bool is_chunk_prefetched(uint32_t chunk_index) const; + + // Returns the next chunk index that should be fetched from the network, given the + // estimated network tip height. Returns UINT32_MAX when nothing to prefetch + // (all chunks present, or the network tip is not far enough ahead). + uint32_t get_next_prefetchable_chunk_index(uint32_t network_height) const; + + // Add a verified chunk to checkpoint.dat (marks it as confirmed) + // This is called after peer consensus is reached (M out of K peers agree) + // Adding to checkpoint.dat IS the confirmation - file only contains verified chunks + bool add_verified_chunk_to_file(uint32_t chunk_index); + + // Get chunk hash for a specific chunk index + crypto::Hash get_chunk_hash(uint32_t chunk_index) const; + + // Get all chunk hashes (for P2P comparison) + std::vector get_all_chunk_hashes() const; + + // Set chunk hashes (from P2P or file) + bool set_chunk_hashes(std::vector&& chunk_hashes); + + // Validate a specific chunk against expected hash + bool validate_chunk(uint32_t chunk_index, const crypto::Hash& expected_hash) const; + + // Chunk confirmation methods + // NOTE: Adding a chunk to checkpoint.dat IS the confirmation + // mark_chunk_confirmed() is a convenience method that also saves to file + // Peer consensus must include only peers with uptime > get_min_peer_uptime_blocks() (network-specific: 12k mainnet, 6k testnet) + // and must sample from different buckets (seed, white list, different IP ranges/ASNs) + void mark_chunk_confirmed(uint32_t chunk_index); + + // Check if a chunk is confirmed + bool is_chunk_confirmed(uint32_t chunk_index) const; + + // Check if node can answer checkpoint requests (requires confirmed chunks) + bool can_answer_checkpoint_requests() const; + + // Get chunks that are in memory but not yet verified (not in checkpoint.dat) + // These are chunks that need peer consensus before being added to file + // Returns: vector of chunk indices that need verification + std::vector get_unverified_chunks() const; + + // Consensus requirements: Simple fixed M/K/n values + // M = minimum agreements required (all M peers must agree) + // K = total peers to sample + // n = minimum distinct /16 networks required + // + // USAGE IN P2P HANDLER: + // 1. Count available peers with uptime > get_min_peer_uptime_blocks() (network-specific) + // 2. Call calculate_consensus_requirements(available_peer_count) to get M, K, n + // 3. Sample K peers ensuring: + // - At least n distinct /16 networks (use get_network_16) + // - Maximum 1 vote per /16 network per chunk (diversity capping) + // 4. Build consensus: require M agreements from K sampled peers + // 5. If consensus reached, call mark_chunk_confirmed() + struct ConsensusRequirements { + uint32_t min_agreements; // M: minimum peers that must agree + uint32_t min_peers; // K: total peers to sample + uint32_t min_diverse_networks; // n: minimum distinct /16 networks required + }; + ConsensusRequirements calculate_consensus_requirements(size_t available_peers) const; + + struct ConsensusVote { + uint64_t peer_id; + crypto::Hash hash; + uint64_t timestamp; + }; + + struct ConsensusVoteResult { + uint32_t responses_received; + uint32_t null_hash_responses; + uint32_t agreements; + uint32_t local_diverse_networks; + crypto::Hash consensus_hash; + uint32_t consensus_hash_votes; + uint32_t consensus_hash_diverse_networks; + bool local_consensus; + bool divergent_consensus; + }; + + static ConsensusVoteResult evaluate_consensus_votes( + const std::vector& votes, + const crypto::Hash& local_hash, + const std::map& peer_network_16, + uint64_t min_response_timestamp, + const ConsensusRequirements& req); + + // Get minimum peer uptime requirement for checkpoint verification (network-specific) + // Mainnet: 12,000 blocks (~16.7 days) + // Testnet: 6,000 blocks (~8.3 days, relaxed for smaller network) + uint32_t get_min_peer_uptime_blocks() const + { + return m_testnet ? cn::P2P_CHECKPOINT_PEER_MIN_UPTIME_BLOCKS_TESTNET + : cn::P2P_CHECKPOINT_PEER_MIN_UPTIME_BLOCKS; + } + + // Network diversity tracking for consensus + // Extract /16 network from IP address (for diversity capping) + // + // USAGE: When building consensus: + // std::map network_votes; // network_16 -> vote_count + // for (each peer response) { + // uint32_t net16 = CheckpointList::get_network_16(peer_ip); + // network_votes[net16] = std::min(network_votes[net16] + 1, 1U); // Cap at 1 vote per /16 + // } + static uint32_t get_network_16(uint32_t ip); + + // Structure to return peer sampling results + struct PeerSamplingResult { + std::vector sampled_peers; + std::map network_votes; // network_16 -> vote_count (capped at 1) + }; + + /** + * Sample peers with network diversity + * + * Randomly samples K peers from available peers, ensuring network diversity + * (max 1 peer per /16 network). Falls back to any available peer if diversity + * cannot be achieved. + * + * @param available_peers List of available peer IDs + * @param getPeerNetwork16Func Function to get /16 network for a peer (peer_id -> network_16) + * @param num_peers_to_sample Number of peers to sample (K) + * @return PeerSamplingResult with sampled peers and network votes + */ + static PeerSamplingResult sample_peers_with_diversity( + const std::vector& available_peers, + std::function getPeerNetwork16Func, + size_t num_peers_to_sample); + + /** + * Fallback peer selection when diversity requirement cannot be met + * + * When we can't find enough diverse peers, relax the diversity requirement + * and select any available peers to reach the target sample size. + * + * @param available_peers List of all available peer IDs + * @param sampled_peers Already selected peers + * @param sampled_peer_ids Lookup set mirroring sampled_peers + * @param target_size Target number of peers to sample + */ + static void fallback_peer_selection( + const std::vector& available_peers, + std::vector& sampled_peers, + std::unordered_set& sampled_peer_ids, + size_t target_size); + + // Health metrics for monitoring + struct HealthMetrics { + uint32_t total_chunks; + uint32_t confirmed_chunks; + double confirmation_ratio; // confirmed / total + uint32_t highest_confirmed; + uint32_t highest_total; + uint32_t unconfirmed_count; // chunks beyond hardcoded checkpoints + }; + HealthMetrics get_health_metrics() const; + + // Validate chunks in checkpoint.dat against checkpoints from CryptoNoteConfig.h + // OPTIMIZATION: Only validates the LAST (highest) checkpoint for fast startup validation + // Previous checkpoints were already validated when they were added + // This is called on startup to detect if maintainer added new checkpoints + // Returns: {is_valid, first_mismatched_chunk_index} + // If invalid, rollback blockchain and truncate checkpoint.dat to last_valid_chunk_index + struct ValidationResult { + bool is_valid; + uint32_t first_mismatched_chunk_index; // Only valid if is_valid == false + }; + ValidationResult validate_chunks_against_checkpoints( + std::function(uint32_t startHeight, uint32_t maxCount)> getBlockIdsFunc, + uint32_t current_blockchain_height) const; + + // Truncate checkpoint.dat to a specific chunk index (removes all chunks after this index) + // Used when validation fails and we need to rollback + // @param last_valid_chunk_index The last chunk index to keep (inclusive) + // @return true if truncation was successful + bool truncate_checkpoint_file(uint32_t last_valid_chunk_index); + + // Verify chunk with peer consensus (with second chance retry) + // This implements the "second chance" mechanism: if first attempt fails, re-sample peers once + // + // @param chunk_index The chunk index to verify + // @param local_chunk_hash The locally computed chunk hash + // @param getPeerChunkHashFunc Function to get chunk hash from a peer (peer_id -> chunk_hash or NULL_HASH if failed) + // @param available_peers List of available peer IDs that meet requirements (uptime > 12k blocks, etc.) + // @param getPeerNetwork16Func Function to get /16 network for a peer (peer_id -> network_16) + // @return ConsensusResult with consensus status and details + struct ConsensusResult { + bool consensus_reached; // true if M peers agreed + uint32_t agreements_first_attempt; // Number of agreements in first attempt + uint32_t agreements_second_attempt; // Number of agreements in second attempt (if retried) + bool used_second_chance; // true if second attempt was made + crypto::Hash consensus_hash_first_attempt; // Hash that M/K peers agreed on in attempt 1 (if different from local, NULL_HASH otherwise) + crypto::Hash consensus_hash_second_attempt; // Hash that M/K peers agreed on in attempt 2 (if different from local, NULL_HASH otherwise) + }; + ConsensusResult verify_chunk_with_peer_consensus( + uint32_t chunk_index, + const crypto::Hash& local_chunk_hash, + std::function getPeerChunkHashFunc, + const std::vector& available_peers, + std::function getPeerNetwork16Func) const; + + // Legacy methods (for backward compatibility during transition) + bool add_checkpoint_list(uint32_t start_height, std::vector& points); + bool set_checkpoint_list(std::vector&& points); + bool load_checkpoints_from_file(); + + // Test helpers (for unit testing only) + bool add_checkpoint_target_for_test(uint32_t height, const std::string& hash_str); + + // Clear all checkpoint state so tests start from a known empty slate + // after init_targets() has loaded network checkpoints from CryptoNoteConfig.h + void clear_targets_for_test() + { + m_targets.clear(); + m_valid_point_sizes.clear(); + m_old_checkpoint_hashes.clear(); + m_dns_checkpoint_hashes.clear(); + { + const std::lock_guard lock(m_points_lock); + m_points.clear(); + } + { + const std::lock_guard lock(m_chunks_lock); + m_chunks.clear(); + m_confirmed_chunks.clear(); + m_prefetched_chunk_indices.clear(); + } + } + + // Get the number of chunks (not individual blocks) + uint32_t get_chunk_count() const + { + const std::lock_guard lock(m_chunks_lock); + return static_cast(m_chunks.size()); + } + + // Get the highest block height covered by chunks + uint32_t get_covered_height() const + { + const std::lock_guard lock(m_chunks_lock); + if (m_chunks.empty()) + return 0; + + // SIMPLIFIED: All chunks are uniform (chunk_size blocks each) + // Block 0 (genesis) is NOT included in chunks + // chunk[0]: covers blocks 1 to chunk_size + // chunk[1]: covers blocks (chunk_size + 1) to (2 * chunk_size) + // chunk[n]: covers blocks (n * chunk_size + 1) to ((n + 1) * chunk_size) + // + // Formula: covered_height = num_chunks * chunk_size + uint32_t num_chunks = static_cast(m_chunks.size()); + return num_chunks * m_chunk_size; + } + + // Get chunk size (blocks per chunk) + uint32_t get_chunk_size() const + { + return m_chunk_size; + } + + // Legacy method for backward compatibility + uint32_t get_points_size() const + { + // Return covered height + 1 (number of blocks, not chunks) + return get_covered_height() + 1; + } + + uint32_t get_greatest_target_height() const + { + uint32_t hardcoded = m_targets.empty() ? 0 : m_targets.rbegin()->first - 1; + uint32_t dns = m_dns_checkpoint_hashes.empty() ? 0 : m_dns_checkpoint_hashes.rbegin()->first; + return hardcoded > dns ? hardcoded : dns; + } + + bool is_ready() const + { + // Check if we have chunks covering up to the greatest target height + return get_covered_height() >= get_greatest_target_height(); + } + + bool is_in_checkpoint_zone(uint32_t height) const + { + // A height is in the checkpoint zone when it is covered by loaded chunk + // hashes — meaning it can be trusted without full ring-sig/input + // verification. Height 0 (genesis) is always outside the zone so it + // is never used to skip validation. + return height > 0 && height <= get_covered_height(); + } + + enum check_rt + { + is_out_of_zone, + is_in_zone_failed, + is_checkpointed + }; + + // Check if a block at given height matches the checkpoint + // + // BACKWARD COMPATIBILITY: This method supports both old (version 1) and new (version 2+) checkpoint systems: + // - Version 1 (old): Validates individual blocks against hardcoded checkpoints from CryptoNoteConfig.h + // Uses m_old_checkpoint_hashes to verify block hash matches expected checkpoint hash + // - Version 2+ (chunked): Uses chunk-based validation + // Individual block validation isn't possible with chunks alone, but chunk integrity ensures chain correctness + check_rt check_checkpoint(uint32_t height, const crypto::Hash& hv) const + { + // Genesis is validated by Blockchain initialization. Core tests replace + // genesis with synthetic blocks, so do not compare height 0 with network + // checkpoint data here. + if (height == 0) + return is_checkpointed; + + // FIRST: Check if this height is a hardcoded checkpoint (from CryptoNoteConfig.h) + // This works for BOTH version 1 and version 2 systems + // m_old_checkpoint_hashes contains individual block hashes for each checkpoint height + // This is read-only access, so no lock needed (m_old_checkpoint_hashes is only written during init) + { + const std::lock_guard lock(m_chunks_lock); // Use existing lock for thread safety + auto old_checkpoint_it = m_old_checkpoint_hashes.find(height); + if (old_checkpoint_it != m_old_checkpoint_hashes.end()) + { + // This is a checkpoint height - validate the block hash matches the expected checkpoint + const crypto::Hash& expected_hash = old_checkpoint_it->second; + if (hv != expected_hash) + { + // Block hash doesn't match hardcoded checkpoint - validation failed + logger(logging::ERROR) << "CheckpointList.cpp: Checkpoint validation FAILED at height " + << height << "! Expected (from CryptoNoteConfig.h): " << expected_hash + << ", Got (from block): " << hv; + return is_in_zone_failed; + } + // Block hash matches checkpoint - validation passed + return is_checkpointed; + } + + // Not a checkpoint height - check if we're in checkpoint zone + // For version 2+ (chunked system), we check if chunks cover this height + // For version 1 (old system), we check if we have checkpoint list covering this height + + // If we have chunks, use chunk-based zone checking + if (!m_chunks.empty()) + { + // For heights >= 1: chunk_index = (height - 1) / chunk_size + uint32_t chunk_index = (height - 1) / m_chunk_size; + + // If we don't have chunks covering this height, it's out of checkpoint zone + if (chunk_index >= m_chunks.size()) + return is_out_of_zone; + + // We have chunks covering this height + // Individual block validation isn't possible with chunks alone, + // but chunk integrity ensures the chain is correct + return is_checkpointed; + } + } + + // Version 1 (old system): Check if we have checkpoint list covering this height + { + const std::lock_guard lock(m_points_lock); + if (m_points.size() > height) + { + // We have checkpoint list covering this height + return is_checkpointed; + } + } + + // No checkpoint coverage for this height + return is_out_of_zone; + } + + std::vector> get_checkpoint_targets() const + { + std::vector> rv; + rv.reserve(m_targets.size()); + for(auto it = m_targets.rbegin(); it != m_targets.rend(); ++it) + rv.emplace_back(it->first-1, it->second); + return rv; + } + + // Legacy method for old checkpoint list system (backward compatibility) + struct t_get_incomplete_checkpoint_target_rv { + crypto::Hash target_hash = NULL_HASH; + uint32_t start_height; + uint32_t end_height; + }; + + t_get_incomplete_checkpoint_target_rv get_incomplete_checkpoint_target() const + { + t_get_incomplete_checkpoint_target_rv rv; + + // REFACTORED: Now works with chunks instead of full checkpoint lists + // Get current coverage from chunks + uint32_t covered_height = get_covered_height(); + uint32_t covered_size = covered_height + 1; // Convert height to size + + if (m_targets.empty()) + return rv; + + uint32_t greatest_target_size = m_targets.rbegin()->first; + + // If we already have chunks covering the greatest target, nothing needed + if(covered_size >= greatest_target_size) + return rv; + + // Find the first target we don't have chunks for + rv.start_height = 0; + for(const auto& p : m_targets) + { + if(covered_size < p.first) + { + rv.end_height = p.first - 1; // Convert size to height + rv.target_hash = p.second; + return rv; + } + else + { + rv.start_height = p.first - 1; // Convert size to height + } + } + return rv; + } + + // New method for chunk-based system: returns which chunks we need + // Returns vector of chunk indices that we don't have yet + std::vector get_incomplete_chunks() const + { + std::vector incomplete_chunks; + + const std::lock_guard lock(m_chunks_lock); + + // Get current coverage + uint32_t covered_height = get_covered_height(); + uint32_t greatest_target_height = get_greatest_target_height(); + + // If we have chunks covering all targets, nothing needed + if (covered_height >= greatest_target_height) + return incomplete_chunks; + + // Calculate which chunks we need + // Start from the first chunk we don't have, up to the chunk containing the greatest target + uint32_t start_chunk_index = height_to_chunk_index(covered_height + 1); + uint32_t end_chunk_index = height_to_chunk_index(greatest_target_height); + + for (uint32_t chunk_index = start_chunk_index; chunk_index <= end_chunk_index; chunk_index++) + { + // Check if we have this chunk + if (chunk_index >= m_chunks.size() || m_chunks[chunk_index] == NULL_HASH) + { + incomplete_chunks.push_back(chunk_index); + } + } + + return incomplete_chunks; + } + + + private: + bool m_testnet; + logging::LoggerRef logger; + std::string m_save_file; + uint32_t m_chunk_size; // Blocks per chunk (CHUNK_SIZE_MAINNET or CHUNK_SIZE_TESTNET) + + // Chunked checkpoint storage: each element is the hash of a chunk of blocks + // SIMPLIFIED: Block 0 (genesis) is NOT included in chunks + // chunk[0] = hash(blocks 1 to chunk_size) + // chunk[1] = hash(blocks chunk_size+1 to 2*chunk_size) + // chunk[n] = hash(blocks n*chunk_size+1 to (n+1)*chunk_size) + mutable std::mutex m_chunks_lock; + std::vector m_chunks; // Chunk hashes (one per chunk_size blocks) + + // Chunk confirmation tracking: only confirmed chunks can be used for rollback + // and for answering checkpoint requests + // Key: chunk_index, Value: true if confirmed via peer consensus + std::unordered_set m_confirmed_chunks; + + // Prefetch tracking: chunk indices whose hash was obtained from P2P consensus + // BEFORE the local chain reached that boundary. When add_chunk_from_block_ids() + // fires for such an index it verifies the local hash against the stored value. + std::unordered_set m_prefetched_chunk_indices; + + // Legacy storage for backward compatibility (will be removed after migration) + mutable std::mutex m_points_lock; + std::vector m_points; // Full block hash list (deprecated, kept for transition) + + // Validation targets: expected hashes at specific checkpoint heights + // Key is height+1 (size), value is the expected hash of blocks 0 to height + std::map m_targets; + std::unordered_set m_valid_point_sizes; // Valid checkpoint sizes (heights+1) + + // Old checkpoint block hashes (individual block hashes from CryptoNoteConfig.h) + // Used for validation during chunk generation to ensure backward compatibility + // Key is height, value is the expected block hash at that height + // PRIORITY 1: Highest priority - hardcoded in source code + std::map m_old_checkpoint_hashes; + + // DNS checkpoint block hashes (individual block hashes from DNS TXT records) + // Used as secondary trusted source during chunk generation + // Key is height, value is the expected block hash at that height + // PRIORITY 2: Secondary priority - fetched from DNS (trusted but can be updated) + std::map m_dns_checkpoint_hashes; + + // Consensus configuration (defined in CryptoNoteConfig.h) + // Using constants from cn namespace for network-wide configuration + + // Save/load chunked checkpoints + bool save_checkpoints(); + bool save_checkpoints_impl(const std::vector& chunks, const std::string& file_path, uint32_t chunk_size); // Private: doesn't lock + bool load_checkpoints_from_file_chunked(); + + // Legacy save/load (for backward compatibility) + bool save_checkpoints_legacy(); + bool load_checkpoints_from_file_legacy(); + + // Migration: Convert legacy format (individual block hashes) to chunked format + bool convert_legacy_to_chunked_format(); + + bool add_checkpoint_target(uint32_t height, const std::string &hash_str); + + // Helper: Calculate which chunk a block height belongs to + // SIMPLIFIED: All chunks are uniform (block 0/genesis excluded) + // chunk[0]: blocks 1 to chunk_size (inclusive) + // chunk[1]: blocks (chunk_size + 1) to (2 * chunk_size) (inclusive) + // chunk[n]: blocks (n * chunk_size + 1) to ((n + 1) * chunk_size) (inclusive) + // + // Formula: chunk_index = (height - 1) / chunk_size + uint32_t height_to_chunk_index(uint32_t height) const + { + return (height - 1) / m_chunk_size; + } + + // Helper: Calculate the start height of a chunk + // SIMPLIFIED: All chunks start at (chunk_index * chunk_size + 1) + // chunk[0]: starts at 1 + // chunk[1]: starts at chunk_size + 1 + // chunk[n]: starts at n * chunk_size + 1 + // + // Formula: start_height = chunk_index * chunk_size + 1 + uint32_t chunk_index_to_start_height(uint32_t chunk_index) const + { + return chunk_index * m_chunk_size + 1; + } + + // Legacy validation (for backward compatibility during transition) + bool is_fsize_valid(uint32_t fsize) + { + if(fsize % sizeof(crypto::Hash) != 0) + return false; + fsize /= sizeof(crypto::Hash); + + return m_valid_point_sizes.find(fsize) != m_valid_point_sizes.end(); + } + + // Internal structure for consensus attempt results + struct AttemptResult { + uint32_t agreements; + crypto::Hash consensus_hash; // Hash that M/K peers agreed on (if different from local, NULL_HASH otherwise) + }; + + /** + * Attempt to reach consensus on a chunk hash with peers + * + * Samples K peers with network diversity, collects their chunk hashes, + * and determines if M peers agree with the local hash. + * + * @param chunk_index The chunk index being validated + * @param local_chunk_hash The locally computed chunk hash + * @param getPeerChunkHashFunc Function to get chunk hash from a peer + * @param available_peers List of available peer IDs + * @param getPeerNetwork16Func Function to get /16 network for a peer + * @param req Consensus requirements (M, K, n) + * @param attempt_name Name of the attempt (for logging) + * @param total_null_hash_responses Reference to counter for NULL_HASH responses (updated) + * @param total_mismatches Reference to counter for mismatches (updated) + * @return AttemptResult with agreement count and consensus hash + */ + AttemptResult attempt_consensus_impl( + uint32_t chunk_index, + const crypto::Hash& local_chunk_hash, + std::function getPeerChunkHashFunc, + const std::vector& available_peers, + std::function getPeerNetwork16Func, + const ConsensusRequirements& req, + const std::string& attempt_name, + uint32_t& total_null_hash_responses, + uint32_t& total_mismatches) const; + + // Internal structure for checkpoint application results + struct CheckpointApplicationResult { + bool success; + uint32_t checkpoints_from_config; + uint32_t checkpoints_from_dns; + std::vector checkpoints_in_chunk; // Heights of checkpoints applied (optional) + }; + + /** + * Apply checkpoint priority to chunk block IDs + * + * Applies checkpoints with priority order: CryptoNoteConfig.h > DNS > blockchain.dat + * Validates that blockchain.dat matches expected checkpoint values before applying. + * + * @param chunk_index The chunk index being processed + * @param chunk_start_height Starting height of the chunk + * @param chunk_end_height Ending height of the chunk + * @param chunk_block_ids Reference to chunk block IDs (modified in place) + * @param getBlockIdsFunc Optional function to re-fetch block IDs for validation (nullptr if not needed) + * @param track_checkpoint_heights If true, tracks which specific checkpoint heights were applied + * @return CheckpointApplicationResult with success status and counters + */ + CheckpointApplicationResult apply_checkpoint_priority_to_chunk( + uint32_t chunk_index, + uint32_t chunk_start_height, + uint32_t chunk_end_height, + std::vector& chunk_block_ids, + std::function(uint32_t startHeight, uint32_t maxCount)> getBlockIdsFunc = nullptr, + bool track_checkpoint_heights = false) const; + + /** + * Validate a checkpoint hash against blockchain data + * + * @param checkpoint_height The checkpoint height to validate + * @param expected_hash The expected hash from checkpoint source (CryptoNoteConfig.h or DNS) + * @param chunk_block_ids Current chunk block IDs (may have been modified by DNS checkpoints) + * @param index_in_chunk Index of checkpoint height within the chunk + * @param getBlockIdsFunc Optional function to re-fetch original blockchain data (nullptr if not needed) + * @return true if validation passed, false otherwise + */ + bool validate_checkpoint_hash( + uint32_t checkpoint_height, + const crypto::Hash& expected_hash, + const std::vector& chunk_block_ids, + uint32_t index_in_chunk, + std::function(uint32_t startHeight, uint32_t maxCount)> getBlockIdsFunc = nullptr) const; + }; +} diff --git a/src/CryptoNoteCore/Checkpoints.cpp b/src/CryptoNoteCore/Checkpoints.cpp deleted file mode 100644 index a75d6dce1..000000000 --- a/src/CryptoNoteCore/Checkpoints.cpp +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright (c) 2011-2017 The Cryptonote developers -// Copyright (c) 2017-2018 The Circle Foundation & Conceal Devs -// Copyright (c) 2018-2023 Conceal Network & Conceal Devs -// -// Copyright (c) 2016-2019, The Karbo developers - -// Distributed under the MIT/X11 software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Checkpoints.h" -#include "../CryptoNoteConfig.h" -#include "Common/StringTools.h" -#include "Common/DnsTools.h" - -using namespace logging; - -namespace cn { -//--------------------------------------------------------------------------- -Checkpoints::Checkpoints(logging::ILogger &log) : logger(log, "checkpoints") {} -//--------------------------------------------------------------------------- -bool Checkpoints::add_checkpoint(uint32_t height, const std::string &hash_str) { - crypto::Hash h = NULL_HASH; - - if (!common::podFromHex(hash_str, h)) { - logger(ERROR) << "<< Checkpoints.cpp << " << "Incorrect hash in checkpoints"; - return false; - } - - if (!(0 == m_points.count(height))) { - logger(DEBUGGING) << "Checkpoint already exists for height " << height; - return false; - } - - m_points[height] = h; - - return true; -} -//--------------------------------------------------------------------------- -bool Checkpoints::is_in_checkpoint_zone(uint32_t height) const { - return !m_points.empty() && (height <= (--m_points.end())->first); -} -//--------------------------------------------------------------------------- -bool Checkpoints::check_block(uint32_t height, const crypto::Hash &h, bool &is_a_checkpoint) const { - auto it = m_points.find(height); - is_a_checkpoint = it != m_points.end(); - if (!is_a_checkpoint) - return true; - - if (it->second == h) { - return true; - } else { - logger(logging::ERROR) << "<< Checkpoints.cpp << " << "Checkpoint failed for height " << height - << ". Expected hash: " << it->second - << ", Fetched hash: " << h; - return false; - } -} -//--------------------------------------------------------------------------- -bool Checkpoints::check_block(uint32_t height, const crypto::Hash &h) const { - bool ignored; - return check_block(height, h, ignored); -} -//--------------------------------------------------------------------------- -bool Checkpoints::is_alternative_block_allowed(uint32_t blockchain_height, uint32_t block_height) const { - if (0 == block_height) - return false; - - uint32_t lowest_height = blockchain_height - cn::parameters::CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW; - - if (blockchain_height < cn::parameters::CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW) - { - lowest_height = 0; - } - - if (block_height < lowest_height && !is_in_checkpoint_zone(block_height)) - { - logger(logging::DEBUGGING, logging::WHITE) - << "<< Checkpoints.cpp << " - << "Reorganization depth too deep : " << (blockchain_height - block_height) << ". Block Rejected"; - return false; - } - - auto it = m_points.upper_bound(blockchain_height); - if (it == m_points.begin()) - return true; - - --it; - uint32_t checkpoint_height = it->first; - return checkpoint_height < block_height; -} - -//--------------------------------------------------------------------------- - -std::vector Checkpoints::getCheckpointHeights() const { - std::vector checkpointHeights; - checkpointHeights.reserve(m_points.size()); - for (const auto& it : m_points) { - checkpointHeights.push_back(it.first); - } - - return checkpointHeights; -} - -bool Checkpoints::load_checkpoints_from_dns() -{ - std::string domain("checkpoints.conceal.id"); - if (m_testnet) - { - domain = "testpoints.conceal.gq"; - } - std::vectorrecords; - - logger(logging::DEBUGGING) << "<< Checkpoints.cpp << " << "Fetching DNS checkpoint records from " << domain; - - if (!common::fetch_dns_txt(domain, records)) { - logger(logging::DEBUGGING) << "<< Checkpoints.cpp << " << "Failed to lookup DNS checkpoint records from " << domain; - } - - for (const auto& record : records) { - uint32_t height; - crypto::Hash hash = NULL_HASH; - std::stringstream ss; - size_t del = record.find_first_of(':'); - std::string height_str = record.substr(0, del), hash_str = record.substr(del + 1, 64); - ss.str(height_str); - ss >> height; - char c; - if (del == std::string::npos) continue; - if ((ss.fail() || ss.get(c)) || !common::podFromHex(hash_str, hash)) { - logger(logging::INFO) << "<< Checkpoints.cpp << " << "Failed to parse DNS checkpoint record: " << record; - continue; - } - - if (!(0 == m_points.count(height))) { - logger(DEBUGGING) << "<< Checkpoints.cpp << " << "Checkpoint already exists for height: " << height << ". Ignoring DNS checkpoint."; - } else { - add_checkpoint(height, hash_str); - logger(DEBUGGING) << "<< Checkpoints.cpp << " << "Added DNS checkpoint: " << height_str << ":" << hash_str; - } - } - - return true; -} - -bool Checkpoints::load_checkpoints() -{ - if (m_testnet) - { - for (const auto &cp : cn::TESTNET_CHECKPOINTS) - { - add_checkpoint(cp.height, cp.blockId); - } - } - else - { - for (const auto &cp : cn::CHECKPOINTS) - { - add_checkpoint(cp.height, cp.blockId); - } - } - return true; -} - -bool Checkpoints::load_checkpoints_from_file(const std::string& fileName) { - std::ifstream file(fileName); - if (!file) { - logger(logging::ERROR, BRIGHT_RED) << "Could not load checkpoints file: " << fileName; - return false; - } - std::string indexString; - std::string hash; - uint32_t height; - while (std::getline(file, indexString, ','), std::getline(file, hash)) { - try { - height = std::stoi(indexString); - } catch (const std::invalid_argument &) { - logger(ERROR, BRIGHT_RED) << "Invalid checkpoint file format - " - << "could not parse height as a number"; - return false; - } - if (!add_checkpoint(height, hash)) { - return false; - } - } - logger(logging::INFO) << "Loaded " << m_points.size() << " checkpoints from " << fileName; - return true; -} - -void Checkpoints::set_testnet(bool testnet) { m_testnet = testnet; } - -} diff --git a/src/CryptoNoteCore/Checkpoints.h b/src/CryptoNoteCore/Checkpoints.h deleted file mode 100644 index 6de13aca1..000000000 --- a/src/CryptoNoteCore/Checkpoints.h +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2011-2017 The Cryptonote developers -// Copyright (c) 2017-2018 The Circle Foundation & Conceal Devs -// Copyright (c) 2018-2023 Conceal Network & Conceal Devs -// -// Distributed under the MIT/X11 software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -#pragma once -#include -#include "CryptoNoteBasicImpl.h" -#include - -namespace cn -{ - class Checkpoints - { - public: - explicit Checkpoints(logging::ILogger& log); - - bool add_checkpoint(uint32_t height, const std::string& hash_str); - bool is_in_checkpoint_zone(uint32_t height) const; - bool load_checkpoints_from_file(const std::string& fileName); - bool load_checkpoints_from_dns(); - bool load_checkpoints(); - bool check_block(uint32_t height, const crypto::Hash& h) const; - bool check_block(uint32_t height, const crypto::Hash& h, bool& is_a_checkpoint) const; - bool is_alternative_block_allowed(uint32_t blockchain_height, uint32_t block_height) const; - std::vector getCheckpointHeights() const; - void set_testnet(bool testnet); - - private: - bool m_testnet = false; - std::map m_points; - logging::LoggerRef logger; - }; -} diff --git a/src/CryptoNoteCore/CheckpointsList.cpp b/src/CryptoNoteCore/CheckpointsList.cpp new file mode 100644 index 000000000..64ea83a2c --- /dev/null +++ b/src/CryptoNoteCore/CheckpointsList.cpp @@ -0,0 +1,2546 @@ +// Copyright (c) 2011-2017 The Cryptonote developers +// Copyright (c) 2017-2018 The Circle Foundation & Conceal Devs +// Copyright (c) 2018-2023 Conceal Network & Conceal Devs +// +// Copyright (c) 2016-2019, The Karbo developers + +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +/** + * CHECKPOINT CHUNKING IMPLEMENTATION + * =================================== + * + * PROBLEM SOLVED: + * - Old approach: Store all 1.87M block hashes (60MB file, expensive to hash/compare) + * - New approach: Store chunk hashes (187 chunks × 32 bytes = ~6KB file) + * + * HOW CHUNKING WORKS: + * 1. Blocks are grouped into chunks (10,000 blocks per chunk for mainnet, 25,000 for testnet) + * 2. Each chunk hash = hash(block_id_0 || block_id_1 || ... || block_id_N) + * 3. checkpoint.dat stores: [chunk0_hash, chunk1_hash, ..., chunk186_hash] + * + * BENEFITS: + * 1. Fast P2P Comparison: + * - Compare 187 chunk hashes (6KB) instead of hashing 60MB + * - Binary search through chunks to find disagreement (log2(187) ≈ 8 comparisons) + * + * 2. Efficient Disagreement Detection: + * - Node A: [chunk0, chunk1, ..., chunk186] + * - Node B: [chunk0, chunk1, ..., chunk186] + * - Compare sequentially: chunk0 matches? chunk1 matches? ... + * - First mismatch → problem is in that chunk's 10,000-block range + * - Request specific chunk's block IDs to find exact block + * + * 3. Small File Size: + * - 6KB vs 60MB (10,000x smaller) + * - Fast to load/save + * + * 4. Efficient Rollback: + * - Know exactly which chunk (10,000-block range) has the problem + * - Rollback to chunk boundary, then sync from there + * + * P2P PROTOCOL: + * 1. Nodes exchange chunk hashes first (fast comparison) + * 2. If chunks match → checkpoints are in sync + * 3. If chunks differ → find first mismatched chunk + * 4. Request block IDs for that specific chunk + * 5. Compare block IDs to find exact disagreement point + * 6. Rollback to last matching chunk boundary + * + * FILE FORMAT: + * - Binary file: [chunk0_hash][chunk1_hash]...[chunkN_hash] + * - Each hash is 32 bytes (crypto::Hash) + * - File size = num_chunks × 32 bytes + * + * VALIDATION: + * - Chunks are validated during generation against hardcoded checkpoints + * - Individual block validation uses chunk integrity + blockchain validation + * - Checkpoint heights (from CryptoNoteConfig.h) are validated against targets + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "CheckpointList.h" +#include "../CryptoNoteConfig.h" +#include "Common/StringTools.h" +#include "Common/DnsTools.h" +#include "crypto/hash.h" +#include + +using namespace logging; + +namespace cn { + void CheckpointList::init_targets(bool is_testnet, const std::string& save_file) + { + m_testnet = is_testnet; + m_save_file = save_file; + + // Set chunk size based on network type + // Mainnet: 10,000 blocks per chunk (matches checkpoint frequency) + // Testnet: 25,000 blocks per chunk (matches checkpoint frequency) + m_chunk_size = is_testnet ? CHUNK_SIZE_TESTNET : CHUNK_SIZE_MAINNET; + + logger(logging::INFO) << "Initializing checkpoint targets with chunk size: " + << m_chunk_size << " blocks per chunk"; + + // Load hardcoded checkpoints from CryptoNoteConfig.h as validation targets + // These serve as the source of truth - all P2P-received checkpoint lists must + // hash to match these target hashes to be considered valid + // + // IMPORTANT: Also store the old checkpoint block hashes for validation during chunk generation + // This ensures backward compatibility - when generating chunks, we validate that + // any checkpoint heights within a chunk match the old checkpoint values from CryptoNoteConfig.h + m_old_checkpoint_hashes.clear(); + for (const auto &cp : m_testnet ? cn::TESTNET_CHECKPOINTS : cn::CHECKPOINTS) + { + crypto::Hash checkpoint_hash = NULL_HASH; + if (common::podFromHex(cp.blockId, checkpoint_hash)) + { + // Store old checkpoint hash (individual block hash at this height) + // This will be used to validate chunks during generation + m_old_checkpoint_hashes[cp.height] = checkpoint_hash; + } + add_checkpoint_target(cp.height, cp.blockId); + } + + logger(INFO) << "Stored " << m_old_checkpoint_hashes.size() + << " checkpoint block hashes from CryptoNoteConfig.h (PRIORITY 1)"; + + // Fetch DNS checkpoint records (PRIORITY 2: secondary trusted source) + const char* domain; + if (m_testnet) + domain = TESTNET_DNS_CHECKPOINT_DOMAIN; + else + domain = DNS_CHECKPOINT_DOMAIN; + + std::vectorrecords; + m_dns_checkpoint_hashes.clear(); + + logger(INFO) << "Fetching DNS checkpoint records from " << domain; + + if (!common::fetch_dns_txt(domain, records)) { + logger(INFO) << "DNS checkpoint lookup failed or no records found for " << domain + << " (this is OK - will use CryptoNoteConfig.h checkpoints and P2P validation)"; + } + else if (records.empty()) + { + logger(INFO) << "DNS checkpoint domain " << domain << " exists but contains no TXT records " + << "(will use CryptoNoteConfig.h checkpoints and P2P validation)"; + } + else + { + logger(INFO) << "Found " << records.size() << " DNS TXT record(s) from " << domain; + + uint32_t dns_checkpoints_added = 0; + uint32_t dns_checkpoints_skipped = 0; + uint32_t dns_checkpoints_invalid = 0; + + for (const auto& record : records) { + // Support both formats: + // 1. One checkpoint per record: "height:hash" + // 2. Multiple checkpoints per record (newline-separated): "height1:hash1\nheight2:hash2\n..." + + std::stringstream record_stream(record); + std::string line; + bool record_has_valid_checkpoint = false; + bool record_has_skipped_checkpoint = false; + uint32_t invalid_lines_in_record = 0; + + while (std::getline(record_stream, line)) { + // Skip empty lines + if (line.empty() || (line.find_first_not_of(" \t\r\n") == std::string::npos)) { + continue; + } + + // Trim whitespace + line.erase(0, line.find_first_not_of(" \t")); + line.erase(line.find_last_not_of(" \t") + 1); + + uint32_t height; + crypto::Hash hash = NULL_HASH; + std::stringstream ss; + size_t del = line.find_first_of(':'); + + if (del == std::string::npos) { + logger(DEBUGGING) << "Invalid DNS checkpoint format (missing ':'): " << line; + invalid_lines_in_record++; + continue; + } + + std::string height_str = line.substr(0, del), hash_str = line.substr(del + 1, 64); + ss.str(height_str); + ss >> height; + char c; + + if ((ss.fail() || ss.get(c)) || !common::podFromHex(hash_str, hash)) { + logger(DEBUGGING) << "Failed to parse DNS checkpoint: " << line; + invalid_lines_in_record++; + continue; + } + + // Check if this height already exists in CryptoNoteConfig.h (PRIORITY 1 takes precedence) + if (m_old_checkpoint_hashes.count(height) > 0) { + logger(DEBUGGING) << "Checkpoint at height " << height << " already exists in CryptoNoteConfig.h. " + << "Skipping DNS checkpoint (CryptoNoteConfig.h takes precedence)."; + dns_checkpoints_skipped++; + record_has_skipped_checkpoint = true; + continue; + } + + // Store DNS checkpoint hash (for use in chunk generation with priority) + m_dns_checkpoint_hashes[height] = hash; + + // Also add as target for validation (if not already a target) + if (m_targets.count(height) == 0) { + add_checkpoint_target(height, hash_str); + } + + dns_checkpoints_added++; + record_has_valid_checkpoint = true; + logger(INFO) << "Added DNS checkpoint (PRIORITY 2): " << height_str << ":" << hash_str; + } + + // Count this record as invalid ONLY if it had invalid lines AND no valid/skipped checkpoints + // (A record with only skipped checkpoints is valid but redundant, not invalid) + if (invalid_lines_in_record > 0 && !record_has_valid_checkpoint && !record_has_skipped_checkpoint && !record.empty()) { + dns_checkpoints_invalid++; + } + } + + if (dns_checkpoints_added > 0) + { + logger(INFO) << "Stored " << m_dns_checkpoint_hashes.size() + << " DNS checkpoint block hashes (PRIORITY 2)" + << " (" << dns_checkpoints_added << " added, " + << dns_checkpoints_skipped << " skipped - already in CryptoNoteConfig.h, " + << dns_checkpoints_invalid << " invalid records)"; + } + else + { + logger(INFO) << "No valid DNS checkpoints added (all " << records.size() + << " record(s) were either invalid, duplicate, or already in CryptoNoteConfig.h)"; + } + } + } + + bool CheckpointList::is_old_style_checkpoint_file() const + { + auto genesis_it = m_old_checkpoint_hashes.find(0); + if (genesis_it == m_old_checkpoint_hashes.end()) + return false; // no genesis checkpoint to compare against + + std::ifstream file(m_save_file, std::ios::binary); + if (!file.is_open()) + return false; // file absent → not old-style + + crypto::Hash first_hash; + if (!file.read(reinterpret_cast(&first_hash), sizeof(first_hash))) + return false; + + return first_hash == genesis_it->second; + } + + void CheckpointList::convert_old_checkpoints_to_list_hashes( + std::function(uint32_t startHeight, uint32_t maxCount)> getBlockIdsFunc, + uint32_t max_height) + { + // If chunks already cover max_height, checkpoint.dat is already new-style — + // no need to convert m_targets (and we must not treat chunk hashes as block hashes). + if (get_covered_height() >= max_height) + { + logger(DEBUGGING) << "Skipping conversion - chunks already cover height " << max_height; + return; + } + + // Store old targets temporarily (they're individual block hashes from CryptoNoteConfig.h) + // Note: m_targets stores size (height+1) as key, so we need to convert back to get heights + // IMPORTANT: m_old_checkpoint_hashes is preserved - it contains the individual block hashes + // needed for validation during chunk generation + std::map old_targets = m_targets; + m_targets.clear(); + m_valid_point_sizes.clear(); + + logger(INFO) << "Converting old-style checkpoints (individual block hashes) " + << "to new-style (list hashes) for P2P compatibility"; + if (max_height != UINT32_MAX) + { + logger(DEBUGGING) << "Only converting checkpoints up to height " << max_height + << " (current blockchain height)"; + } + + uint32_t converted_count = 0; + uint32_t skipped_count = 0; + + // Pre-count how many checkpoints are within range so the spinner can show X/N + uint32_t convertible_count = 0; + for (const auto& t : old_targets) + { + if ((t.first - 1) <= max_height) + convertible_count++; + } + + static const char spinner_chars[] = {'|', '/', '-', '\\'}; + uint32_t spin_idx = 0; + + // For each old checkpoint, compute the list hash from genesis to that height + for (const auto& old_target : old_targets) + { + uint32_t size = old_target.first; // This is already height+1 (stored by add_checkpoint_target) + uint32_t height = size - 1; + + // Skip checkpoints beyond the current blockchain height + if (height > max_height) + { + skipped_count++; + continue; + } + + std::fprintf(stderr, "\r [%c] Converting checkpoint %u/%u (height %u)... ", + spinner_chars[spin_idx % 4], converted_count + 1, convertible_count, height); + std::fflush(stderr); + ++spin_idx; + + // Get block IDs from genesis (0) to this height + std::vector blockIds = getBlockIdsFunc(0, size); + + if (blockIds.size() != size) + { + logger(WARNING) << "Could not get all block IDs for height " + << height << " (got " << blockIds.size() << ", expected " << size << "). Skipping."; + continue; + } + + // VALIDATION: Before converting, verify that the block hash at this checkpoint height + // matches the old checkpoint value from CryptoNoteConfig.h + // This ensures the blockchain matches the expected checkpoints + auto old_checkpoint_it = m_old_checkpoint_hashes.find(height); + if (old_checkpoint_it != m_old_checkpoint_hashes.end()) + { + const crypto::Hash& expected_hash = old_checkpoint_it->second; + const crypto::Hash& actual_hash = blockIds[height]; // blockIds[height] is the hash at that height + + if (actual_hash != expected_hash) + { + logger(ERROR) << "CHECKPOINT VALIDATION FAILED at height " << height + << "! Expected (from CryptoNoteConfig.h): " << expected_hash + << ", Got (from blockchain.dat): " << actual_hash + << ". Cannot convert checkpoints - blockchain doesn't match expected values."; + // Don't clear m_targets - keep old format for now + return; + } + + logger(DEBUGGING) << "Validated checkpoint at height " << height + << " before conversion (matches CryptoNoteConfig.h)"; + } + + // Compute hash of the list of block IDs from genesis to this height + crypto::Hash listHash = crypto::cn_fast_hash(blockIds.data(), blockIds.size() * sizeof(crypto::Hash)); + + // Store as new target (list hash instead of individual block hash) + m_targets[size] = listHash; + m_valid_point_sizes.insert(size); + converted_count++; + + logger(DEBUGGING) << "Converted checkpoint at height " << height + << " (old hash: " << old_target.second << ") to list hash: " << listHash; + } + + // Clear the spinner line before the final log message + std::fprintf(stderr, "\r%60s\r", ""); + std::fflush(stderr); + + logger(INFO) << "Converted " << converted_count + << " checkpoint validation targets for P2P compatibility" + << " (preserved " << m_old_checkpoint_hashes.size() + << " block hashes for chunk validation)"; + if (skipped_count > 0) + { + logger(DEBUGGING) << "Skipped " << skipped_count + << " checkpoints beyond current blockchain height (will convert as blockchain syncs)"; + } + } + + /** + * Generate checkpoint chunks from block IDs + * + * CHUNKING EXPLANATION: + * Instead of storing all 1.87M block hashes (60MB), we store chunk hashes: + * NOTE: Block 0 (genesis) is EXCLUDED from chunks + * - Chunk 0: hash(blocks 1-10000) -> 1 hash (32 bytes) + * - Chunk 1: hash(blocks 10001-20000) -> 1 hash (32 bytes) + * - ... + * - Chunk 186: hash(blocks 1860001-1870000) -> 1 hash (32 bytes) + * + * Total: 187 chunks × 32 bytes = ~6KB (vs 60MB for full list) + * + * Benefits: + * 1. Fast P2P comparison: Compare 187 chunk hashes instead of hashing 60MB + * 2. Fast disagreement detection: Binary search through chunks (log2(187) ≈ 8 comparisons) + * 3. Efficient rollback: Know exactly which 10,000-block range has the problem + * 4. Small file size: 6KB vs 60MB + * + * VALIDATION PROCESS: + * Before creating each chunk hash, we validate against old checkpoints from CryptoNoteConfig.h: + * 1. Check if any checkpoint heights (e.g., 290665, 290674, 290675, 290676, 290720) fall within the chunk + * 2. Validate that the block hash at those checkpoint heights matches CryptoNoteConfig.h values + * 3. Only if validation passes, create the chunk hash + * + * This ensures: + * - Correctness: blockchain.dat matches expected checkpoints + * - Compatibility: Smooth transition from version 1 to version 2 + * - Security: Invalid or corrupted blockchains cannot create valid chunks + * + * @param getBlockIdsFunc Function to retrieve block IDs from the blockchain + * @param max_height Maximum block height to generate chunks for + * @return true if chunks were successfully generated and validated + */ + bool CheckpointList::generate_chunks_from_block_ids( + std::function(uint32_t startHeight, uint32_t maxCount)> getBlockIdsFunc, + uint32_t max_height) + { + std::vector chunks_to_save; + std::string file_to_save; + uint32_t chunk_size; + + // Generate chunks while holding lock + { + const std::lock_guard lock(m_chunks_lock); + + // Calculate how many chunks we need + // chunk[0]: blocks 1 to chunk_size (inclusive) = chunk_size blocks + // chunk[1]: blocks (chunk_size + 1) to (2 * chunk_size) (inclusive) = chunk_size blocks + // chunk[n]: blocks (n * chunk_size + 1) to ((n + 1) * chunk_size) (inclusive) = chunk_size blocks + // + // If max_height = 1,870,000 and chunk_size = 10,000: + // - chunk[0]: 1-10000 (10,000 blocks) + // - chunk[1]: 10001-20000 (10,000 blocks) + // - ... + // - chunk[186]: 1860001-1870000 (10,000 blocks) + // So we need 187 chunks (0-186) + // + // Formula: num_chunks = max_height / chunk_size + uint32_t num_chunks = max_height / m_chunk_size; + + logger(INFO) << "Generating " << num_chunks + << " checkpoint chunks (chunk size: " << m_chunk_size + << ", max height: " << max_height << ") - block 0 (genesis) excluded"; + + m_chunks.clear(); + m_chunks.reserve(num_chunks); + + // Generate each chunk + for (uint32_t chunk_index = 0; chunk_index < num_chunks; chunk_index++) + { + // chunk[0]: blocks 1 to chunk_size + // chunk[1]: blocks (chunk_size + 1) to (2 * chunk_size) + // chunk[n]: blocks (n * chunk_size + 1) to ((n + 1) * chunk_size) + uint32_t chunk_start_height = chunk_index * m_chunk_size + 1; + uint32_t chunk_end_height = std::min((chunk_index + 1) * m_chunk_size, max_height); + uint32_t blocks_in_chunk = chunk_end_height - chunk_start_height + 1; + + // Get all block IDs for this chunk from blockchain.dat + std::vector chunk_block_ids = getBlockIdsFunc(chunk_start_height, blocks_in_chunk); + + if (chunk_block_ids.size() != blocks_in_chunk) + { + logger(ERROR) << "Failed to get all block IDs for chunk " + << chunk_index << " (got " << chunk_block_ids.size() + << ", expected " << blocks_in_chunk << ")"; + m_chunks.clear(); + return false; + } + + // Apply checkpoint priority: CryptoNoteConfig.h > DNS > blockchain.dat + CheckpointApplicationResult checkpoint_result = apply_checkpoint_priority_to_chunk( + chunk_index, + chunk_start_height, + chunk_end_height, + chunk_block_ids, + getBlockIdsFunc, + true); // track_checkpoint_heights = true for detailed logging + + if (!checkpoint_result.success) + { + m_chunks.clear(); + return false; + } + + // Log validation success if there were checkpoints in this chunk + if (!checkpoint_result.checkpoints_in_chunk.empty()) + { + std::stringstream ss; + ss << "Validated and replaced " << checkpoint_result.checkpoints_in_chunk.size() << " checkpoint(s) in chunk " << chunk_index + << " (blocks " << chunk_start_height << "-" << chunk_end_height << ", heights: "; + for (size_t i = 0; i < checkpoint_result.checkpoints_in_chunk.size(); i++) { + if (i > 0) ss << ", "; + ss << checkpoint_result.checkpoints_in_chunk[i]; + } + ss << ") - " << checkpoint_result.checkpoints_from_config << " from CryptoNoteConfig.h, " + << checkpoint_result.checkpoints_from_dns << " from DNS, rest from blockchain.dat"; + logger(INFO) << ss.str(); + } + + // Compute hash of this chunk's block IDs + // This is the chunk hash: hash(block_id_0 || block_id_1 || ... || block_id_N) + crypto::Hash chunk_hash = crypto::cn_fast_hash( + chunk_block_ids.data(), + chunk_block_ids.size() * sizeof(crypto::Hash) + ); + + m_chunks.push_back(chunk_hash); + + if ((chunk_index + 1) % 10 == 0 || chunk_index == num_chunks - 1) + { + logger(INFO) << "Generated chunk " + << (chunk_index + 1) << "/" << num_chunks + << " (blocks " << chunk_start_height << "-" << chunk_end_height << ")"; + } + } + + logger(INFO) << "Successfully generated " + << m_chunks.size() << " checkpoint chunks"; + + // Mark all chunks as confirmed (they were validated against hardcoded checkpoints) + // Chunks generated from hardcoded checkpoints are automatically confirmed + for (uint32_t i = 0; i < m_chunks.size(); i++) + { + m_confirmed_chunks.insert(i); + } + + logger(INFO) << "Marked " << m_chunks.size() + << " chunks as confirmed (validated against CryptoNoteConfig.h)"; + + // Copy data before releasing lock (to avoid deadlock when calling save_checkpoints_impl) + chunks_to_save = m_chunks; + file_to_save = m_save_file; + chunk_size = m_chunk_size; + } // Lock released here + + // Now save without holding the lock + return save_checkpoints_impl(chunks_to_save, file_to_save, chunk_size); + } + + /** + * Add a single chunk (called when a new chunk_size blocks are added) + * + * This is called every 10,000 blocks (mainnet) or 25,000 blocks (testnet) + * to compute a new chunk hash and store it in memory. + * + * CRITICAL: This does NOT save to checkpoint.dat. The chunk is stored in memory only. + * After peer consensus is reached, call add_verified_chunk_to_file() to save it. + * Adding to checkpoint.dat IS the confirmation - the file only contains verified chunks. + * + * @param getBlockIdsFunc Function to retrieve block IDs from the blockchain + * @param chunk_start_height The starting height of the chunk to add + * @return true if chunk hash was successfully computed and stored in memory + */ + bool CheckpointList::add_chunk_from_block_ids( + std::function(uint32_t startHeight, uint32_t maxCount)> getBlockIdsFunc, + uint32_t chunk_start_height, + bool* out_prefetch_mismatch) + { + if (out_prefetch_mismatch) *out_prefetch_mismatch = false; + + bool do_auto_confirm = false; // set inside lock when prefetch matches local hash + uint32_t auto_confirm_chunk_index = 0; + + { + const std::lock_guard lock(m_chunks_lock); + + // All chunks are uniform: chunk_size blocks each (block 0/genesis excluded) + // chunk[0]: blocks 1 to chunk_size + // chunk[1]: blocks (chunk_size + 1) to (2 * chunk_size) + // chunk[n]: blocks (n * chunk_size + 1) to ((n + 1) * chunk_size) + uint32_t chunk_index = (chunk_start_height - 1) / m_chunk_size; + uint32_t chunk_end_height = (chunk_index + 1) * m_chunk_size; + uint32_t blocks_in_chunk = m_chunk_size; + + std::vector chunk_block_ids = getBlockIdsFunc(chunk_start_height, blocks_in_chunk); + + if (chunk_block_ids.size() != blocks_in_chunk) + { + logger(ERROR) << "Failed to get all block IDs for chunk " + << chunk_index << " (got " << chunk_block_ids.size() + << ", expected " << blocks_in_chunk << ")"; + return false; + } + + // Apply checkpoint priority: CryptoNoteConfig.h > DNS > blockchain.dat + CheckpointApplicationResult checkpoint_result = apply_checkpoint_priority_to_chunk( + chunk_index, + chunk_start_height, + chunk_end_height, + chunk_block_ids, + getBlockIdsFunc, + false); + + if (!checkpoint_result.success) + return false; + + uint32_t total_checkpoints_applied = checkpoint_result.checkpoints_from_config + checkpoint_result.checkpoints_from_dns; + if (total_checkpoints_applied > 0) + { + logger(INFO) << "Applied " << total_checkpoints_applied + << " checkpoint(s) to chunk " << chunk_index + << " (blocks " << chunk_start_height << "-" << chunk_end_height << "): " + << checkpoint_result.checkpoints_from_config << " from CryptoNoteConfig.h, " + << checkpoint_result.checkpoints_from_dns << " from DNS, rest from blockchain.dat"; + } + + crypto::Hash local_hash = crypto::cn_fast_hash( + chunk_block_ids.data(), + chunk_block_ids.size() * sizeof(crypto::Hash) + ); + + // --- Prefetch verification --- + // If this chunk was previously prefetched from the network, verify the local + // computation matches. A match lets us confirm immediately; a mismatch means + // our local chain diverged from the network — queue for consensus validation + // which will trigger a rollback if the network disagrees. + bool was_prefetched = (m_prefetched_chunk_indices.find(chunk_index) != m_prefetched_chunk_indices.end()); + if (was_prefetched) + { + crypto::Hash prefetched_hash = (chunk_index < m_chunks.size()) ? m_chunks[chunk_index] : NULL_HASH; + + if (local_hash == prefetched_hash) + { + // Match: local chain agrees with network consensus. Schedule confirmation. + m_prefetched_chunk_indices.erase(chunk_index); + m_chunks[chunk_index] = local_hash; // same value, but now locally owned + do_auto_confirm = true; + auto_confirm_chunk_index = chunk_index; + logger(INFO, logging::BRIGHT_GREEN) + << "Chunk " << chunk_index + << " locally verified — matches prefetched consensus hash. Confirming to checkpoint.dat."; + } + else + { + // Mismatch: local chain diverges from what the network agreed on. + // Replace the prefetched hash with the local one and remove the prefetch + // mark so validate_unverified_chunks() picks it up for consensus. + // If the network still disagrees, divergent_consensus will trigger a rollback. + logger(ERROR, logging::BRIGHT_RED) + << "Chunk " << chunk_index << " PREFETCH MISMATCH: " + << "local=" << local_hash << " prefetched=" << prefetched_hash + << ". Queueing for consensus validation (rollback likely)."; + m_prefetched_chunk_indices.erase(chunk_index); + m_chunks[chunk_index] = local_hash; + if (out_prefetch_mismatch) *out_prefetch_mismatch = true; + // Return true — chunk is stored; validate_unverified_chunks() handles rollback. + } + } + else + { + // Normal path: no prefetch record for this chunk. + if (chunk_index >= m_chunks.size()) + m_chunks.resize(chunk_index + 1); + m_chunks[chunk_index] = local_hash; + logger(INFO) << "Computed chunk " << chunk_index + << " hash (blocks " << chunk_start_height << "-" << chunk_end_height << ")" + << " - stored in memory (pending peer consensus)"; + } + } // m_chunks_lock released + + // Auto-confirm outside the lock (add_verified_chunk_to_file takes its own lock) + if (do_auto_confirm) + { + if (!add_verified_chunk_to_file(auto_confirm_chunk_index)) + { + logger(ERROR) << "Failed to save auto-confirmed prefetch chunk " + << auto_confirm_chunk_index << " to checkpoint.dat"; + } + } + + return true; + } + + /** + * Add a verified chunk to checkpoint.dat (marks it as confirmed) + * + * This is called after peer consensus is reached (M out of K peers agree). + * Adding a chunk to checkpoint.dat IS the confirmation - the file only contains verified chunks. + * + * @param chunk_index The chunk index to add to checkpoint.dat + * @return true if chunk was successfully added to file + */ + bool CheckpointList::add_verified_chunk_to_file(uint32_t chunk_index) + { + crypto::Hash chunk_hash; + std::vector chunks_to_save; + std::string file_to_save; + uint32_t chunk_size; + bool append_only = false; + + { + const std::lock_guard lock(m_chunks_lock); + + // Verify chunk exists in memory + if (chunk_index >= m_chunks.size() || m_chunks[chunk_index] == NULL_HASH) + { + logger(ERROR) << "Cannot add chunk " << chunk_index + << " to checkpoint.dat: chunk not found in memory"; + return false; + } + + chunk_hash = m_chunks[chunk_index]; + file_to_save = m_save_file; + chunk_size = m_chunk_size; + + // Fast path: verified chunks are sequential, so adding the next chunk only + // needs to append one hash. Rewrites are still used for non-append cases. + std::ifstream file(m_save_file, std::ios::binary | std::ios::ate); + if (file.is_open()) + { + uint64_t fsize = file.tellg(); + + if (fsize > 0 && fsize % sizeof(crypto::Hash) == 0) + { + uint32_t num_existing_chunks = static_cast(fsize / sizeof(crypto::Hash)); + append_only = (chunk_index == num_existing_chunks); + } + else if (fsize == 0) + { + append_only = (chunk_index == 0); + } + file.close(); + } + else + { + append_only = (chunk_index == 0); + } + + if (!append_only) + { + // Get current chunks from file (only verified chunks are in file) + // We need to load existing verified chunks and rewrite the file. + std::vector verified_chunks; + + std::ifstream rewrite_file(m_save_file, std::ios::binary | std::ios::ate); + if (rewrite_file.is_open()) + { + uint64_t fsize = rewrite_file.tellg(); + rewrite_file.seekg(0, std::ios::beg); + + if (fsize > 0 && fsize % sizeof(crypto::Hash) == 0) + { + uint32_t num_existing_chunks = static_cast(fsize / sizeof(crypto::Hash)); + verified_chunks.resize(num_existing_chunks); + rewrite_file.read(reinterpret_cast(verified_chunks.data()), fsize); + } + rewrite_file.close(); + } + + // Ensure verified_chunks has enough space for the new chunk + if (chunk_index >= verified_chunks.size()) + { + verified_chunks.resize(chunk_index + 1, NULL_HASH); + } + + // Add the verified chunk + verified_chunks[chunk_index] = chunk_hash; + chunks_to_save = verified_chunks; + } + + } // Lock released here + + bool success = false; + if (append_only) + { + std::ofstream file(file_to_save, std::ios::binary | std::ios::app); + if (!file.is_open()) + { + logger(ERROR) << "Error opening checkpoint file for append: " << file_to_save; + return false; + } + + file.write(reinterpret_cast(&chunk_hash), sizeof(crypto::Hash)); + file.close(); + + if (!file) + { + logger(ERROR) << "Error appending to checkpoint file: " << file_to_save; + return false; + } + + uint32_t covered_height = (chunk_index + 1) * chunk_size; + logger(INFO) << "Appended checkpoint chunk " << chunk_index + << " to file (covers up to height " << covered_height << ")"; + success = true; + } + else + { + success = save_checkpoints_impl(chunks_to_save, file_to_save, chunk_size); + } + + if (success) + { + const std::lock_guard lock(m_chunks_lock); + // Mark as confirmed (chunks in checkpoint.dat are confirmed) + m_confirmed_chunks.insert(chunk_index); + } + + return success; + } + + // --------------------------------------------------------------------------- + // Forward-prefetch methods + // --------------------------------------------------------------------------- + + /** + * Store a chunk hash that was obtained from P2P consensus before the local chain + * has reached that chunk boundary. Extends is_in_checkpoint_zone() so IBD can + * continue at fast speed. The hash is verified locally when + * add_chunk_from_block_ids() fires at the corresponding chunk boundary. + */ + bool CheckpointList::add_prefetched_chunk(uint32_t chunk_index, const crypto::Hash& consensus_hash) + { + if (consensus_hash == NULL_HASH) + { + logger(WARNING) << "Prefetch rejected for chunk " << chunk_index << ": consensus_hash is NULL"; + return false; + } + + const std::lock_guard lock(m_chunks_lock); + + // Don't overwrite a chunk that has already been locally computed (not prefetched). + if (chunk_index < m_chunks.size() && m_chunks[chunk_index] != NULL_HASH + && m_prefetched_chunk_indices.find(chunk_index) == m_prefetched_chunk_indices.end()) + { + logger(DEBUGGING) << "Prefetch skipped for chunk " << chunk_index + << ": already has a locally-computed hash"; + return false; + } + + if (chunk_index >= m_chunks.size()) + m_chunks.resize(chunk_index + 1, NULL_HASH); + + m_chunks[chunk_index] = consensus_hash; + m_prefetched_chunk_indices.insert(chunk_index); + + uint32_t covered_up_to = (chunk_index + 1) * m_chunk_size; + logger(INFO, logging::BRIGHT_CYAN) + << "Prefetched chunk " << chunk_index + << " from P2P consensus (covers up to block " << covered_up_to + << ", hash: " << consensus_hash << ")"; + return true; + } + + /** + * Returns true when the chunk at chunk_index holds a hash obtained from P2P + * consensus rather than local block-id computation. + */ + bool CheckpointList::is_chunk_prefetched(uint32_t chunk_index) const + { + const std::lock_guard lock(m_chunks_lock); + return m_prefetched_chunk_indices.find(chunk_index) != m_prefetched_chunk_indices.end(); + } + + /** + * Returns the index of the next chunk that should be fetched from the network. + * A chunk can be prefetched only when the network tip is high enough for that + * chunk to be complete (network_height >= (chunk_index + 1) * chunk_size). + * Returns UINT32_MAX when nothing to prefetch. + */ + uint32_t CheckpointList::get_next_prefetchable_chunk_index(uint32_t network_height) const + { + const std::lock_guard lock(m_chunks_lock); + + // Find the first index with a missing/null entry. + uint32_t next = static_cast(m_chunks.size()); // default: one past the end + for (uint32_t i = 0; i < static_cast(m_chunks.size()); i++) + { + if (m_chunks[i] == NULL_HASH) { next = i; break; } + } + + // The chunk is complete on the network only when network_height covers all its blocks: + // chunk 'next' covers blocks (next * chunk_size + 1) to (next + 1) * chunk_size + // → complete when network_height >= (next + 1) * chunk_size + uint64_t required = static_cast(next + 1) * m_chunk_size; + if (static_cast(network_height) < required) + return UINT32_MAX; + + return next; + } + + // --------------------------------------------------------------------------- + + /** + * Get the hash of a specific chunk + * + * @param chunk_index The chunk index (0-based) + * @return The chunk hash, or NULL_HASH if chunk doesn't exist + */ + crypto::Hash CheckpointList::get_chunk_hash(uint32_t chunk_index) const + { + const std::lock_guard lock(m_chunks_lock); + + if (chunk_index >= m_chunks.size()) + return NULL_HASH; + + return m_chunks[chunk_index]; + } + + /** + * Get all chunk hashes (for P2P comparison) + * + * This allows nodes to quickly compare their checkpoint chunks: + * 1. Node A sends: [chunk0_hash, chunk1_hash, ..., chunk186_hash] + * 2. Node B compares: chunk0 matches? chunk1 matches? ... + * 3. First mismatch found -> problem is in that chunk's 10,000-block range + * 4. Request specific chunk's block IDs to find exact block + * + * @return Vector of all chunk hashes + */ + std::vector CheckpointList::get_all_chunk_hashes() const + { + const std::lock_guard lock(m_chunks_lock); + return m_chunks; // Return copy + } + + /** + * Set chunk hashes (from P2P or file) + * + * @param chunk_hashes Vector of chunk hashes to set + * @return true if successfully set + */ + bool CheckpointList::set_chunk_hashes(std::vector&& chunk_hashes) + { + std::vector chunks_to_save; + std::string file_to_save; + uint32_t chunk_size; + + // Set chunks while holding lock + { + const std::lock_guard lock(m_chunks_lock); + + // Basic validation: should have at least some chunks + if (chunk_hashes.empty()) + { + logger(WARNING) << "Received empty chunk list"; + return false; + } + + m_chunks = std::move(chunk_hashes); + logger(INFO) << "Set " << m_chunks.size() << " checkpoint chunks"; + + // Copy data before releasing lock (to avoid deadlock when calling save_checkpoints_impl) + chunks_to_save = m_chunks; + file_to_save = m_save_file; + chunk_size = m_chunk_size; + } // Lock released here + + // Save to file without holding lock + return save_checkpoints_impl(chunks_to_save, file_to_save, chunk_size); + } + + /** + * Validate a specific chunk against expected hash + * + * Used during P2P comparison: when a chunk mismatch is found, + * validate the chunk to confirm the disagreement. + * + * @param chunk_index The chunk index to validate + * @param expected_hash The expected hash for this chunk + * @return true if chunk hash matches expected hash + */ + bool CheckpointList::validate_chunk(uint32_t chunk_index, const crypto::Hash& expected_hash) const + { + const std::lock_guard lock(m_chunks_lock); + + if (chunk_index >= m_chunks.size()) + { + logger(WARNING) << "Chunk index " << chunk_index + << " out of range (have " << m_chunks.size() << " chunks)"; + return false; + } + + bool matches = (m_chunks[chunk_index] == expected_hash); + + if (!matches) + { + logger(WARNING) << "Chunk " << chunk_index + << " mismatch: expected " << expected_hash + << ", got " << m_chunks[chunk_index]; + } + + return matches; + } + + bool CheckpointList::add_checkpoint_target(uint32_t height, const std::string &hash_str) { + crypto::Hash h = NULL_HASH; + + if (!common::podFromHex(hash_str, h)) { + logger(ERROR) << "Incorrect hash in checkpoints"; + return false; + } + + if (!(0 == m_targets.count(height))) { + logger(INFO) << "Checkpoint already exists for height " << height; + return false; + } + + height += 1; + m_targets[height] = h; + m_valid_point_sizes.insert(height); + + return true; + } + + bool CheckpointList::add_checkpoint_target_for_test(uint32_t height, const std::string& hash_str) { + return add_checkpoint_target(height, hash_str); + } + + bool CheckpointList::set_checkpoint_list(std::vector&& points) + { + const std::lock_guard lock(m_points_lock); + uint32_t point_size = points.size(); + if(m_valid_point_sizes.find(point_size) == m_valid_point_sizes.end()) + return false; + + // Validate the checkpoint list against the hardcoded target from CryptoNoteConfig.h + // The target hash is the expected hash of the continuous list of block hashes from height 0 + crypto::Hash hv = crypto::cn_fast_hash(points.data(), points.size() * sizeof(crypto::Hash)); + if(hv != m_targets[point_size]) + { + logger(ERROR) << "CheckpointList verification failed for height " << point_size-1 << + ". Expected hash (from hardcoded checkpoints): " << m_targets[point_size] << + ", Fetched hash: " << hv; + return false; + } + + m_points = std::move(points); + logger(INFO) << "Loaded " << m_points.size() << " checkpoints from local index"; + + save_checkpoints(); + return true; + } + + bool CheckpointList::add_checkpoint_list(uint32_t start_height, std::vector& points) + { + const std::lock_guard lock(m_points_lock); + + if(m_points.size() != start_height) + return true; + + uint32_t point_size = points.size() + m_points.size(); + if(m_valid_point_sizes.find(point_size) == m_valid_point_sizes.end()) + return false; + + /* This copy wouldn't be needed with three funciton hash */ + std::vector new_points(m_points); + new_points.insert(std::end(new_points), std::begin(points), std::end(points)); + + // Validate the combined checkpoint list against the hardcoded target from CryptoNoteConfig.h + // This ensures P2P-received checkpoint lists match the expected values + crypto::Hash hv = crypto::cn_fast_hash(new_points.data(), new_points.size() * sizeof(crypto::Hash)); + if(hv != m_targets[point_size]) + { + logger(ERROR) << "CheckpointList verification failed for height " << point_size-1 << + ". Expected hash (from hardcoded checkpoints): " << m_targets[point_size] << + ", Fetched hash: " << hv; + return false; + } + + m_points = std::move(new_points); + logger(INFO) << "Loaded " << points.size() << " checkpoints from p2p, total " << m_points.size(); + + save_checkpoints(); + return true; + } + + /** + * Load checkpoints from file (chunked format) + * + * FILE FORMAT: + * - Binary file containing chunk hashes + * - Each chunk hash is 32 bytes (crypto::Hash) + * - File size = num_chunks × 32 bytes + * + * Example: 187 chunks = 187 × 32 = 5,984 bytes (~6KB) + * vs old format: 1,870,000 blocks × 32 = 59,840,000 bytes (~60MB) + */ + bool CheckpointList::load_checkpoints_from_file_chunked() + { + std::ifstream file(m_save_file, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + // File doesn't exist - this is normal on first run or after deleting checkpoint.dat + logger(DEBUGGING) << "checkpoint.dat not found (will generate from blockchain if needed)"; + return false; + } + + uint64_t fsize = file.tellg(); + + // Validate file size: must be multiple of hash size + if (fsize % sizeof(crypto::Hash) != 0) { + logger(ERROR) << "Invalid checkpoint file size: " << fsize + << " (not multiple of " << sizeof(crypto::Hash) << ")"; + return false; + } + + uint32_t num_chunks = static_cast(fsize / sizeof(crypto::Hash)); + + if (num_chunks == 0) { + logger(WARNING) << "Checkpoint file is empty"; + return false; + } + + file.seekg(0, std::ios::beg); + + std::vector chunks(num_chunks); + if (!file.read(reinterpret_cast(chunks.data()), fsize)) { + logger(ERROR) << "Error reading checkpoint file"; + return false; + } + + const std::lock_guard lock(m_chunks_lock); + m_chunks = std::move(chunks); + + // CRITICAL: All chunks in checkpoint.dat are automatically confirmed + // The file only contains verified chunks (up to hardcoded checkpoints + peer-verified chunks) + // On restart, everything in checkpoint.dat is 100% reliable - no re-validation needed + m_confirmed_chunks.clear(); + for (uint32_t i = 0; i < m_chunks.size(); i++) + { + m_confirmed_chunks.insert(i); + } + + // Calculate covered height + // SIMPLIFIED: All chunks are uniform (block 0 excluded) + // chunk[0]: covers blocks 1 to chunk_size (chunk_size blocks) + // chunk[1]: covers blocks (chunk_size + 1) to (2 * chunk_size) (chunk_size blocks) + // chunk[n]: covers blocks (n * chunk_size + 1) to ((n + 1) * chunk_size) (chunk_size blocks) + // Formula: covered_height = num_chunks * chunk_size + uint32_t covered_height = num_chunks * m_chunk_size; + + uint32_t greatest_target_height = get_greatest_target_height(); + // Calculate which chunk the greatest target belongs to + // Formula: chunk_index = (height - 1) / chunk_size + uint32_t greatest_target_chunk = (greatest_target_height - 1) / m_chunk_size; + + // Calculate hardcoded covered height (up to greatest target chunk) + // SIMPLIFIED: hardcoded_end = (greatest_target_chunk + 1) * chunk_size + uint32_t hardcoded_end = (greatest_target_chunk + 1) * m_chunk_size; + uint32_t hardcoded_covered_height = std::min(hardcoded_end, covered_height); + + logger(INFO) << "Loaded " << num_chunks + << " checkpoint chunks from disk (covers up to height " << covered_height << ")"; + logger(INFO) << "Auto-confirmed ALL " << m_confirmed_chunks.size() + << " chunks from checkpoint.dat (100% reliable on restart)" + << " - up to height " << hardcoded_covered_height + << " from hardcoded checkpoints, beyond that from peer consensus"; + return true; + } + + /** + * Save checkpoints to file (chunked format) + * + * Writes chunk hashes to binary file for fast loading and P2P sharing. + */ + bool CheckpointList::save_checkpoints() + { + std::vector chunks_to_save; + std::string file_to_save; + uint32_t chunk_size; + + // Copy data while holding lock + { + const std::lock_guard lock(m_chunks_lock); + + if (m_chunks.empty()) { + logger(WARNING) << "No chunks to save"; + return false; + } + + // Copy data before releasing lock (to avoid deadlock when calling save_checkpoints_impl) + chunks_to_save = m_chunks; + file_to_save = m_save_file; + chunk_size = m_chunk_size; + } // Lock released here + + // Save without holding lock + return save_checkpoints_impl(chunks_to_save, file_to_save, chunk_size); + } + + // Private implementation that doesn't lock (assumes caller handles locking) + // NOTE: Only saves chunk hashes, NOT confirmation status (security: minimal disk footprint) + // Confirmation status is restored on load: chunks up to last hardcoded checkpoint are auto-confirmed, + // remaining chunks are validated via P2P consensus after restart + bool CheckpointList::save_checkpoints_impl(const std::vector& chunks, const std::string& file_path, uint32_t chunk_size) + { + if (chunks.empty()) { + logger(WARNING) << "No chunks to save"; + return false; + } + + std::ofstream file(file_path, std::ios::binary); + + if (!file.is_open()) { + logger(ERROR) << "Error opening checkpoint file for write: " << file_path; + return false; + } + + // Write all chunk hashes to file + // NOTE: We do NOT save confirmation status - this is a security feature: + // - Minimal disk footprint (less information that could be used against us) + // - Confirmation is restored on load (chunks up to hardcoded checkpoints are auto-confirmed) + // - Remaining chunks are validated via P2P consensus after restart + file.write(reinterpret_cast(chunks.data()), chunks.size() * sizeof(crypto::Hash)); + file.close(); + + if (!file) { + logger(ERROR) << "Error writing to checkpoint file: " << file_path; + return false; + } + + uint32_t covered_height = static_cast(chunks.size()) * chunk_size; + logger(INFO) << "Saved " << chunks.size() + << " checkpoint chunks to file (covers up to height " << covered_height << ")"; + return true; + } + + /** + * Load checkpoints from file (with automatic format detection) + * + * Tries to load chunked format first, falls back to legacy format if needed. + */ + bool CheckpointList::load_checkpoints_from_file() + { + // Try chunked format first (new format) + if (load_checkpoints_from_file_chunked()) { + return true; + } + + // Fall back to legacy format (for backward compatibility during transition) + // Note: This will also fail if checkpoint.dat doesn't exist at all + logger(DEBUGGING) << "Chunked format load failed, checking for legacy format"; + return load_checkpoints_from_file_legacy(); + } + + /** + * Load checkpoints from file (legacy format - full block hash list) + * + * This is kept for backward compatibility during the transition period. + * Old nodes may have checkpoint.dat files with full block hash lists. + */ + bool CheckpointList::load_checkpoints_from_file_legacy() + { + std::ifstream file(m_save_file, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + logger(DEBUGGING) << "No checkpoint.dat file found (neither chunked nor legacy format)"; + return false; + } + + uint64_t fsize = file.tellg(); + if(!is_fsize_valid(fsize)) { + logger(ERROR) << "Invalid file size" << fsize; + return false; + } + uint32_t point_size = fsize / sizeof(crypto::Hash); + + file.seekg(0, std::ios::beg); + + std::vector points(point_size); + if (!file.read(reinterpret_cast(points.data()), fsize)) { + logger(ERROR) << "error reading file"; + return false; + } + + // Validate the checkpoint list loaded from file against the hardcoded target from CryptoNoteConfig.h + // This ensures the saved checkpoint list matches the expected values from the source code + crypto::Hash hv = crypto::cn_fast_hash(points.data(), points.size() * sizeof(crypto::Hash)); + if(hv != m_targets[point_size]) + { + logger(ERROR) << "CheckpointList verification (from file) failed for height " << point_size-1 + << ". Expected hash (from hardcoded checkpoints): " << m_targets[point_size] + << ", File hash: " << hv; + return false; + } + + const std::lock_guard lock(m_points_lock); + m_points = std::move(points); + logger(INFO) << "Loaded " << m_points.size() << " checkpoints from disk (legacy format) " << m_save_file; + + // Automatically convert legacy format to chunked format (only for version 2+) + // Version 1 nodes continue using legacy format + if (cn::P2P_CURRENT_VERSION >= cn::P2P_CHECKPOINT_LIST_VERSION) { + if (convert_legacy_to_chunked_format()) { + logger(INFO) << "Successfully converted legacy checkpoint format to chunked format"; + + // Backup old file before saving new format + std::string backup_file = m_save_file + ".bckv1"; + std::ifstream src(m_save_file, std::ios::binary); + if (src.good()) { + std::ofstream dst(backup_file, std::ios::binary); + if (dst.good()) { + dst << src.rdbuf(); + logger(INFO) << "Backed up legacy checkpoint file to " << backup_file; + } else { + logger(WARNING) << "Failed to create backup file " << backup_file; + } + } + + // Save in new chunked format (overwrites old file) + if (save_checkpoints()) { + logger(INFO) << "Migration complete: saved chunked format, legacy format backed up to " << backup_file; + } else { + logger(ERROR) << "Migration failed: could not save chunked format"; + return true; // Still return true - legacy format is loaded + } + } else { + logger(WARNING) << "Failed to convert legacy format to chunked format - keeping legacy format"; + } + } else { + logger(INFO) << "Node is version 1 - keeping legacy checkpoint format (migration only for version 2+)"; + } + + return true; + } + + /** + * Convert legacy format (individual block hashes) to chunked format + * + * Legacy format: m_points[i] = block hash at height i (i=0 is genesis, i=1 is block 1, etc.) + * Chunked format: m_chunks[j] = hash of all block hashes in chunk j (chunks exclude block 0) + * + * Conversion: + * - Skip block 0 (genesis) - chunks don't include it + * - chunk[0] = hash(m_points[1] || m_points[2] || ... || m_points[chunk_size]) + * - chunk[1] = hash(m_points[chunk_size+1] || m_points[chunk_size+2] || ... || m_points[2*chunk_size]) + * - etc. + */ + bool CheckpointList::convert_legacy_to_chunked_format() + { + std::lock_guard points_lock(m_points_lock); + + if (m_points.empty()) { + logger(WARNING) << "Cannot convert: legacy format is empty"; + return false; + } + + // Legacy format: m_points[0] = block 0 (genesis), m_points[1] = block 1, etc. + // Chunks exclude block 0, so we start from m_points[1] + uint32_t total_blocks = static_cast(m_points.size()); + if (total_blocks < 2) { + logger(WARNING) << "Cannot convert: need at least block 0 and block 1 (have " << total_blocks << " blocks)"; + return false; + } + + // Number of blocks to convert (excluding block 0/genesis) + uint32_t blocks_to_convert = total_blocks - 1; + + // Calculate number of chunks + uint32_t num_chunks = blocks_to_convert / m_chunk_size; + + if (num_chunks == 0) { + logger(WARNING) << "Cannot convert: not enough blocks for even one chunk (have " << blocks_to_convert + << " blocks, need " << m_chunk_size << " per chunk)"; + return false; + } + + logger(INFO) << "Converting legacy format to chunked format: " << total_blocks + << " blocks (" << blocks_to_convert << " excluding genesis) -> " + << num_chunks << " chunks"; + + // Convert to chunks + std::lock_guard chunks_lock(m_chunks_lock); + m_chunks.clear(); + m_chunks.reserve(num_chunks); + + for (uint32_t chunk_index = 0; chunk_index < num_chunks; chunk_index++) { + uint32_t start_block_index = chunk_index * m_chunk_size + 1; // +1 to skip block 0 + uint32_t end_block_index = std::min(start_block_index + m_chunk_size, total_blocks); + uint32_t blocks_in_chunk = end_block_index - start_block_index; + + if (blocks_in_chunk != m_chunk_size && chunk_index < num_chunks - 1) { + // Last chunk can be partial, but intermediate chunks must be full + logger(ERROR) << "Invalid chunk " << chunk_index << ": expected " << m_chunk_size + << " blocks, got " << blocks_in_chunk; + return false; + } + + // Extract block hashes for this chunk + std::vector chunk_block_ids; + chunk_block_ids.reserve(blocks_in_chunk); + for (uint32_t i = start_block_index; i < end_block_index; i++) { + chunk_block_ids.push_back(m_points[i]); + } + + // Calculate chunk hash + crypto::Hash chunk_hash = crypto::cn_fast_hash( + chunk_block_ids.data(), + chunk_block_ids.size() * sizeof(crypto::Hash) + ); + + m_chunks.push_back(chunk_hash); + } + + // Mark all converted chunks as confirmed (they came from validated legacy format) + m_confirmed_chunks.clear(); + for (uint32_t i = 0; i < m_chunks.size(); i++) { + m_confirmed_chunks.insert(i); + } + + logger(INFO) << "Successfully converted " << num_chunks << " chunks from legacy format"; + return true; + } + + /** + * Save checkpoints to file (legacy format - for backward compatibility) + */ + bool CheckpointList::save_checkpoints_legacy() + { + const std::lock_guard lock(m_points_lock); + + if (m_points.empty()) { + return false; + } + + std::ofstream file(m_save_file, std::ios::binary); + + if (!file.is_open()) { + logger(ERROR) << "error opening file for write " << m_save_file; + return false; + } + + file.write(reinterpret_cast(m_points.data()), m_points.size() * sizeof(crypto::Hash)); + file.close(); + + if (!file) { + logger(ERROR) << "error writing to file " << m_save_file; + return false; + } + + return true; + } + + void CheckpointList::mark_chunk_confirmed(uint32_t chunk_index) + { + // Adding to checkpoint.dat IS the confirmation + // This method is a convenience wrapper that calls add_verified_chunk_to_file() + if (!add_verified_chunk_to_file(chunk_index)) + { + logger(ERROR) << "Failed to mark chunk " << chunk_index + << " as confirmed (failed to add to checkpoint.dat)"; + return; + } + + // Log health metrics + HealthMetrics metrics = get_health_metrics(); + + logger(INFO) << "Marked chunk " << chunk_index + << " as confirmed (added to checkpoint.dat via peer consensus with peers having uptime > " + << get_min_peer_uptime_blocks() << " blocks)"; + + // Log health metrics every 10 chunks or on important milestones + if (chunk_index % 10 == 0 || chunk_index == metrics.highest_total) + { + logger(INFO) << "Checkpoint Health: " + << metrics.confirmed_chunks << "/" << metrics.total_chunks + << " chunks confirmed (" << (metrics.confirmation_ratio * 100.0) << "%), " + << "highest confirmed: " << metrics.highest_confirmed + << ", unconfirmed beyond hardcoded: " << metrics.unconfirmed_count; + } + } + + bool CheckpointList::is_chunk_confirmed(uint32_t chunk_index) const + { + const std::lock_guard lock(m_chunks_lock); + return m_confirmed_chunks.find(chunk_index) != m_confirmed_chunks.end(); + } + + bool CheckpointList::can_answer_checkpoint_requests() const + { + const std::lock_guard lock(m_chunks_lock); + + // Node can only answer checkpoint requests if it has confirmed chunks + // This ensures we don't serve unverified data to other nodes + if (m_chunks.empty()) + return false; + + // Check if we have at least some confirmed chunks + // For safety, we require at least the first chunk to be confirmed + // (chunks generated from hardcoded checkpoints are automatically confirmed) + return !m_confirmed_chunks.empty(); + } + + std::vector CheckpointList::get_unverified_chunks() const + { + const std::lock_guard lock(m_chunks_lock); + + std::vector unverified; + + // Find chunks that are in memory but not confirmed (not in checkpoint.dat) + for (uint32_t i = 0; i < m_chunks.size(); i++) + { + if (m_chunks[i] != NULL_HASH && m_confirmed_chunks.find(i) == m_confirmed_chunks.end()) + { + unverified.push_back(i); + } + } + + return unverified; + } + + CheckpointList::ConsensusRequirements CheckpointList::calculate_consensus_requirements(size_t available_peers) const + { + ConsensusRequirements req; + + // Simple fixed consensus requirements (M, K, n) based on network type + // M = minimum agreements required (all M peers must agree) + // K = total peers to sample + // n = minimum distinct /16 networks required + if (m_testnet) + { + req.min_agreements = cn::CKPT_MIN_CONSENSUS_PEERS_TESTNET; // M + req.min_peers = cn::CKPT_CONSENSUS_PEERS_TESTNET; // K + req.min_diverse_networks = cn::CKPT_MIN_DIVERSE_NETWORKS_TESTNET; // n + } + else + { + req.min_agreements = cn::CKPT_MIN_CONSENSUS_PEERS; // M + req.min_peers = cn::CKPT_CONSENSUS_PEERS; // K + req.min_diverse_networks = cn::CKPT_MIN_DIVERSE_NETWORKS; // n + } + + // Warn if not enough peers available + if (available_peers < req.min_peers) + { + logger(WARNING) << "Not enough peers available (" << available_peers + << ") for consensus (need K=" << req.min_peers << " for " << (m_testnet ? "testnet" : "mainnet") << ")"; + } + + return req; + } + + CheckpointList::ConsensusVoteResult CheckpointList::evaluate_consensus_votes( + const std::vector& votes, + const crypto::Hash& local_hash, + const std::map& peer_network_16, + uint64_t min_response_timestamp, + const ConsensusRequirements& req) + { + ConsensusVoteResult result; + result.responses_received = 0; + result.null_hash_responses = 0; + result.agreements = 0; + result.local_diverse_networks = 0; + result.consensus_hash = NULL_HASH; + result.consensus_hash_votes = 0; + result.consensus_hash_diverse_networks = 0; + result.local_consensus = false; + result.divergent_consensus = false; + + std::unordered_map> hash_votes; + std::set local_networks; + std::unordered_map, boost::hash> networks_by_hash; + + for (const ConsensusVote& vote : votes) + { + if (vote.timestamp < min_response_timestamp) + { + continue; + } + + result.responses_received++; + + if (vote.hash == NULL_HASH) + { + result.null_hash_responses++; + continue; + } + + hash_votes[vote.hash]++; + + auto network_it = peer_network_16.find(vote.peer_id); + uint32_t network = (network_it != peer_network_16.end()) ? network_it->second : 0; + + if (vote.hash == local_hash) + { + result.agreements++; + local_networks.insert(network); + } + else + { + networks_by_hash[vote.hash].insert(network); + } + } + + result.local_diverse_networks = static_cast(local_networks.size()); + result.local_consensus = (result.agreements >= req.min_agreements && + result.local_diverse_networks >= req.min_diverse_networks); + + for (const auto& vote_count : hash_votes) + { + if (vote_count.first == local_hash || vote_count.second < req.min_agreements) + { + continue; + } + + auto network_it = networks_by_hash.find(vote_count.first); + uint32_t diverse_networks = (network_it != networks_by_hash.end()) ? + static_cast(network_it->second.size()) : 0; + + if (diverse_networks < req.min_diverse_networks) + { + continue; + } + + if (!result.divergent_consensus || vote_count.second > result.consensus_hash_votes) + { + result.divergent_consensus = true; + result.consensus_hash = vote_count.first; + result.consensus_hash_votes = vote_count.second; + result.consensus_hash_diverse_networks = diverse_networks; + } + } + + return result; + } + + uint32_t CheckpointList::get_network_16(uint32_t ip) + { + // Extract /16 network prefix: first 16 bits of IP address + // For IPv4: network = (ip >> 16) & 0xFFFF + return (ip >> 16) & 0xFFFF; + } + + CheckpointList::PeerSamplingResult CheckpointList::sample_peers_with_diversity( + const std::vector& available_peers, + std::function getPeerNetwork16Func, + size_t num_peers_to_sample) + { + PeerSamplingResult result; + + if (available_peers.empty() || num_peers_to_sample == 0) + { + return result; + } + + // Limit sampling to available peers count + size_t actual_sample_size = std::min(num_peers_to_sample, available_peers.size()); + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution dis(0, available_peers.size() - 1); + std::unordered_set sampled_peer_ids; + + // Sample K peers with network diversity (max 1 vote per /16 network) + while (result.sampled_peers.size() < actual_sample_size) + { + // Try to find a peer from a different network + size_t attempts = 0; + const size_t max_attempts = available_peers.size() * 2; // Prevent infinite loop + + while (attempts < max_attempts && result.sampled_peers.size() < actual_sample_size) + { + size_t idx = dis(gen); + uint64_t peer_id = available_peers[idx]; + + // Check if we already sampled this peer + if (sampled_peer_ids.find(peer_id) != sampled_peer_ids.end()) + { + attempts++; + continue; + } + + // Check network diversity (max 1 vote per /16 network) + uint32_t net16 = getPeerNetwork16Func(peer_id); + if (result.network_votes[net16] >= 1 && result.sampled_peers.size() < available_peers.size()) + { + // Already have a vote from this network, try another peer + attempts++; + continue; + } + + // Accept this peer + result.sampled_peers.push_back(peer_id); + sampled_peer_ids.insert(peer_id); + result.network_votes[net16] = std::min(result.network_votes[net16] + 1, 1U); + break; + } + + // If we couldn't find enough diverse peers, relax diversity requirement + if (result.sampled_peers.size() < actual_sample_size && attempts >= max_attempts) + { + fallback_peer_selection(available_peers, result.sampled_peers, sampled_peer_ids, actual_sample_size); + } + } + + return result; + } + + void CheckpointList::fallback_peer_selection( + const std::vector& available_peers, + std::vector& sampled_peers, + std::unordered_set& sampled_peer_ids, + size_t target_size) + { + // Fall back to any available peer (diversity requirement relaxed) + for (uint64_t peer_id : available_peers) + { + if (sampled_peer_ids.find(peer_id) == sampled_peer_ids.end()) + { + sampled_peers.push_back(peer_id); + sampled_peer_ids.insert(peer_id); + if (sampled_peers.size() >= target_size) + break; + } + } + } + + CheckpointList::HealthMetrics CheckpointList::get_health_metrics() const + { + HealthMetrics metrics; + uint32_t greatest_target_chunk; + + // Get greatest target height first (doesn't require lock) + uint32_t greatest_target_height = get_greatest_target_height(); + // SIMPLIFIED: Calculate which chunk the greatest target belongs to + // Formula: chunk_index = (height - 1) / chunk_size + greatest_target_chunk = (greatest_target_height - 1) / m_chunk_size; + + // Now acquire lock to read chunk data + { + const std::lock_guard lock(m_chunks_lock); + + metrics.total_chunks = static_cast(m_chunks.size()); + metrics.confirmed_chunks = static_cast(m_confirmed_chunks.size()); + + if (metrics.total_chunks > 0) + { + metrics.confirmation_ratio = static_cast(metrics.confirmed_chunks) / metrics.total_chunks; + } + else + { + metrics.confirmation_ratio = 0.0; + } + + // Find highest confirmed chunk + metrics.highest_confirmed = 0; + for (uint32_t chunk_index : m_confirmed_chunks) + { + if (chunk_index > metrics.highest_confirmed) + metrics.highest_confirmed = chunk_index; + } + + metrics.highest_total = metrics.total_chunks > 0 ? metrics.total_chunks - 1 : 0; + + // Calculate unconfirmed chunks (beyond hardcoded checkpoints) + uint32_t confirmed_from_hardcoded = 0; + for (uint32_t i = 0; i <= greatest_target_chunk && i < m_chunks.size(); i++) + { + if (m_confirmed_chunks.find(i) != m_confirmed_chunks.end()) + confirmed_from_hardcoded++; + } + + metrics.unconfirmed_count = metrics.confirmed_chunks - confirmed_from_hardcoded; + } + + return metrics; + } + + /** + * Validate chunks in checkpoint.dat against checkpoints from CryptoNoteConfig.h and DNS + * + * REFINED STRATEGY: Validates the highest checkpoint from both sources (if available) + * Priority: CryptoNoteConfig.h > DNS > blockchain.dat + * + * Process: + * 1. Find the highest checkpoint in CryptoNoteConfig.h (PRIORITY 1) + * 2. Find the highest checkpoint in DNS (PRIORITY 2, if available) + * 3. For each checkpoint we can verify (based on current blockchain height): + * - Compute chunk hash using priority order (CryptoNoteConfig.h > DNS > blockchain.dat) + * - Compare with stored chunk hash in checkpoint.dat + * 4. If mismatch, return the mismatched chunk index + * + * Example: + * - Blockchain height: 1,800,000 + * - CryptoNoteConfig.h highest: 1,700,000 (chunk 170) + * - DNS highest: 1,900,000 (chunk 190, but we can't verify yet - not synced) + * - We can only verify chunk 170 (CryptoNoteConfig.h checkpoint) + * + * @param getBlockIdsFunc Function to retrieve block IDs from blockchain + * @param current_blockchain_height Current blockchain height (to know what we can verify) + * @return ValidationResult with is_valid flag and first_mismatched_chunk_index if invalid + */ + CheckpointList::ValidationResult CheckpointList::validate_chunks_against_checkpoints( + std::function(uint32_t startHeight, uint32_t maxCount)> getBlockIdsFunc, + uint32_t current_blockchain_height) const + { + ValidationResult result; + result.is_valid = true; + result.first_mismatched_chunk_index = 0; + + const std::lock_guard lock(m_chunks_lock); + + // If no chunks loaded, validation passes (nothing to validate) + if (m_chunks.empty()) + { + return result; + } + + // Find highest checkpoints from both sources + uint32_t highest_config_checkpoint_height = 0; + crypto::Hash highest_config_checkpoint_hash = NULL_HASH; + + for (const auto& checkpoint : m_old_checkpoint_hashes) + { + if (checkpoint.first > highest_config_checkpoint_height) + { + highest_config_checkpoint_height = checkpoint.first; + highest_config_checkpoint_hash = checkpoint.second; + } + } + + uint32_t highest_dns_checkpoint_height = 0; + crypto::Hash highest_dns_checkpoint_hash = NULL_HASH; + + for (const auto& checkpoint : m_dns_checkpoint_hashes) + { + if (checkpoint.first > highest_dns_checkpoint_height) + { + highest_dns_checkpoint_height = checkpoint.first; + highest_dns_checkpoint_hash = checkpoint.second; + } + } + + // Determine which checkpoints we can verify (must be <= current_blockchain_height) + std::vector>> checkpoints_to_verify; + + if (highest_config_checkpoint_height > 0 && highest_config_checkpoint_height <= current_blockchain_height) + { + checkpoints_to_verify.push_back(std::make_pair(highest_config_checkpoint_height, + std::make_pair(highest_config_checkpoint_hash, "CryptoNoteConfig.h"))); + } + + // Only verify DNS checkpoint if it's higher than config checkpoint and we're synced to it + if (highest_dns_checkpoint_height > highest_config_checkpoint_height && + highest_dns_checkpoint_height <= current_blockchain_height) + { + checkpoints_to_verify.push_back(std::make_pair(highest_dns_checkpoint_height, + std::make_pair(highest_dns_checkpoint_hash, "DNS"))); + } + + if (checkpoints_to_verify.empty()) + { + // No checkpoints to verify (either none exist or we haven't synced to them yet) + if (highest_config_checkpoint_height > current_blockchain_height) + { + logger(INFO) << "Highest checkpoint from CryptoNoteConfig.h (" << highest_config_checkpoint_height + << ") is beyond current blockchain height (" << current_blockchain_height + << "). Will validate when synced to that height."; + } + if (highest_dns_checkpoint_height > current_blockchain_height) + { + logger(INFO) << "Highest checkpoint from DNS (" << highest_dns_checkpoint_height + << ") is beyond current blockchain height (" << current_blockchain_height + << "). Will validate when synced to that height."; + } + return result; + } + + // Validate each checkpoint we can verify + for (const auto& checkpoint_entry : checkpoints_to_verify) + { + uint32_t checkpoint_height = checkpoint_entry.first; + const std::string& source = checkpoint_entry.second.second; + + // SIMPLIFIED: Find which chunk this checkpoint belongs to + // Formula: chunk_index = (height - 1) / chunk_size + uint32_t chunk_index = (checkpoint_height - 1) / m_chunk_size; + + // Only validate if we have this chunk in checkpoint.dat + if (chunk_index >= m_chunks.size()) + { + logger(INFO) << "Checkpoint from " << source << " at height " + << checkpoint_height << " (chunk " << chunk_index + << ") is beyond our chunks (have " << m_chunks.size() + << " chunks). Will validate when we reach this height."; + continue; // Skip this checkpoint, try next one + } + + logger(INFO) << "Validating checkpoint from " << source << " at height " + << checkpoint_height << " (chunk " << chunk_index << ") against checkpoint.dat"; + + // SIMPLIFIED: Compute chunk boundaries (all chunks uniform, block 0 excluded) + uint32_t chunk_start_height = chunk_index * m_chunk_size + 1; + uint32_t chunk_end_height = (chunk_index + 1) * m_chunk_size; + uint32_t blocks_in_chunk = m_chunk_size; + + // Get block IDs for this chunk from blockchain + std::vector chunk_block_ids = getBlockIdsFunc(chunk_start_height, blocks_in_chunk); + + if (chunk_block_ids.size() != blocks_in_chunk) + { + logger(WARNING) << "Cannot validate chunk " << chunk_index + << ": failed to get all block IDs (got " << chunk_block_ids.size() + << ", expected " << blocks_in_chunk << ")"; + continue; // Skip this checkpoint, try next one + } + + // Apply priority order: CryptoNoteConfig.h > DNS > blockchain.dat + CheckpointApplicationResult checkpoint_result = apply_checkpoint_priority_to_chunk( + chunk_index, + chunk_start_height, + chunk_end_height, + chunk_block_ids, + nullptr, // No re-fetch needed - we already have the data + false); // No need to track specific heights + + if (!checkpoint_result.success) + { + result.is_valid = false; + result.first_mismatched_chunk_index = chunk_index; + return result; + } + + // Compute chunk hash using priority order + crypto::Hash computed_chunk_hash = crypto::cn_fast_hash( + chunk_block_ids.data(), + chunk_block_ids.size() * sizeof(crypto::Hash) + ); + + const crypto::Hash& stored_chunk_hash = m_chunks[chunk_index]; + + if (computed_chunk_hash != stored_chunk_hash) + { + logger(ERROR) << "CHUNK VALIDATION FAILED: " + << "Chunk " << chunk_index << " (contains checkpoint from " << source + << " at height " << checkpoint_height << ") hash mismatch!" + << " Computed (using priority: CryptoNoteConfig.h > DNS > blockchain.dat): " << computed_chunk_hash + << ", Stored in checkpoint.dat: " << stored_chunk_hash + << ". This indicates checkpoint.dat is out of sync."; + result.is_valid = false; + result.first_mismatched_chunk_index = chunk_index; + return result; + } + + logger(INFO) << "Successfully validated checkpoint from " << source + << " at height " << checkpoint_height << " (chunk " << chunk_index + << ") against checkpoint.dat"; + } + + return result; + } + + /** + * Truncate checkpoint.dat to a specific chunk index + * + * This removes all chunks after last_valid_chunk_index from both the file and memory. + * Used when validation fails and we need to rollback. + * + * @param last_valid_chunk_index The last chunk index to keep (inclusive) + * @return true if truncation was successful + */ + bool CheckpointList::truncate_checkpoint_file(uint32_t last_valid_chunk_index) + { + std::vector chunks_to_save; + std::string file_to_save; + uint32_t chunk_size; + + { + const std::lock_guard lock(m_chunks_lock); + + // Ensure we don't truncate beyond what we have + if (last_valid_chunk_index >= m_chunks.size()) + { + logger(WARNING) << "Cannot truncate to chunk " << last_valid_chunk_index + << ": only have " << m_chunks.size() << " chunks"; + return false; + } + + // Calculate how many chunks we're removing (before truncating) + uint32_t original_chunk_count = static_cast(m_chunks.size()); + uint32_t chunks_to_remove = (last_valid_chunk_index < original_chunk_count) + ? (original_chunk_count - (last_valid_chunk_index + 1)) + : 0; + + // Truncate chunks in memory + m_chunks.resize(last_valid_chunk_index + 1); + + // Update confirmed chunks (remove chunks beyond truncation point) + std::unordered_set new_confirmed_chunks; + for (uint32_t chunk_index : m_confirmed_chunks) + { + if (chunk_index <= last_valid_chunk_index) + { + new_confirmed_chunks.insert(chunk_index); + } + } + m_confirmed_chunks = std::move(new_confirmed_chunks); + + logger(WARNING) << "Truncating checkpoint.dat to chunk " << last_valid_chunk_index + << " (removed " << chunks_to_remove << " chunks)"; + + // Copy data before releasing lock + chunks_to_save = m_chunks; + file_to_save = m_save_file; + chunk_size = m_chunk_size; + } // Lock released here + + // Save truncated chunks to file + bool success = save_checkpoints_impl(chunks_to_save, file_to_save, chunk_size); + + if (success) + { + uint32_t covered_height = (last_valid_chunk_index + 1) * chunk_size; + logger(INFO) << "Successfully truncated checkpoint.dat to chunk " + << last_valid_chunk_index << " (covers up to height " << covered_height << ")"; + } + + return success; + } + + /** + * Apply checkpoint priority to chunk block IDs + * + * Applies checkpoints with priority order: CryptoNoteConfig.h > DNS > blockchain.dat + * Validates that blockchain.dat matches expected checkpoint values before applying. + */ + CheckpointList::CheckpointApplicationResult CheckpointList::apply_checkpoint_priority_to_chunk( + uint32_t chunk_index, + uint32_t chunk_start_height, + uint32_t chunk_end_height, + std::vector& chunk_block_ids, + std::function(uint32_t startHeight, uint32_t maxCount)> getBlockIdsFunc, + bool track_checkpoint_heights) const + { + CheckpointApplicationResult result; + result.success = true; + result.checkpoints_from_config = 0; + result.checkpoints_from_dns = 0; + + // STEP 1: Apply DNS checkpoints (PRIORITY 2) + // These will be overwritten by CryptoNoteConfig.h if there's a conflict + for (const auto& checkpoint : m_dns_checkpoint_hashes) + { + uint32_t checkpoint_height = checkpoint.first; + if (checkpoint_height >= chunk_start_height && checkpoint_height <= chunk_end_height) + { + uint32_t index_in_chunk = checkpoint_height - chunk_start_height; + + if (index_in_chunk >= chunk_block_ids.size()) + { + logger(ERROR) << "DNS checkpoint height " << checkpoint_height + << " index out of range in chunk " << chunk_index; + result.success = false; + return result; + } + + // VALIDATION: Verify that blockchain.dat matches the DNS checkpoint + const crypto::Hash& dns_hash = checkpoint.second; + + if (!validate_checkpoint_hash(checkpoint_height, dns_hash, chunk_block_ids, index_in_chunk, nullptr)) + { + logger(ERROR) << "DNS CHECKPOINT VALIDATION FAILED for chunk " + << chunk_index << " at height " << checkpoint_height << "!" + << " Expected (from DNS): " << dns_hash; + result.success = false; + return result; + } + + // Replace with DNS checkpoint hash + chunk_block_ids[index_in_chunk] = dns_hash; + result.checkpoints_from_dns++; + + if (track_checkpoint_heights) + { + result.checkpoints_in_chunk.push_back(checkpoint_height); + } + + logger(DEBUGGING) << "Applied DNS checkpoint hash for height " + << checkpoint_height << " in chunk " << chunk_index; + } + } + + // STEP 2: Apply CryptoNoteConfig.h checkpoints (PRIORITY 1 - HIGHEST) + // These overwrite both blockchain.dat and DNS checkpoints + for (const auto& checkpoint : m_old_checkpoint_hashes) + { + uint32_t checkpoint_height = checkpoint.first; + if (checkpoint_height >= chunk_start_height && checkpoint_height <= chunk_end_height) + { + uint32_t index_in_chunk = checkpoint_height - chunk_start_height; + + if (index_in_chunk >= chunk_block_ids.size()) + { + logger(ERROR) << "CryptoNoteConfig.h checkpoint height " << checkpoint_height + << " index out of range in chunk " << chunk_index; + result.success = false; + return result; + } + + // VALIDATION: Verify that blockchain.dat matches the CryptoNoteConfig.h checkpoint + const crypto::Hash& config_hash = checkpoint.second; + + if (!validate_checkpoint_hash(checkpoint_height, config_hash, chunk_block_ids, index_in_chunk, getBlockIdsFunc)) + { + logger(ERROR) << "CryptoNoteConfig.h CHECKPOINT VALIDATION FAILED for chunk " + << chunk_index << " at height " << checkpoint_height << "!" + << " Expected (from CryptoNoteConfig.h): " << config_hash; + result.success = false; + return result; + } + + // Replace with CryptoNoteConfig.h checkpoint hash (overwrites DNS if it was applied) + chunk_block_ids[index_in_chunk] = config_hash; + + // Update counters + if (track_checkpoint_heights) + { + // Check if this height was already in the list (from DNS) + auto it = std::find(result.checkpoints_in_chunk.begin(), result.checkpoints_in_chunk.end(), checkpoint_height); + if (it == result.checkpoints_in_chunk.end()) + { + result.checkpoints_in_chunk.push_back(checkpoint_height); + } + else + { + // This checkpoint was already applied from DNS, now overwritten by CryptoNoteConfig.h + result.checkpoints_from_dns--; // Remove DNS count, add config count + } + } + result.checkpoints_from_config++; + + logger(DEBUGGING) << "Applied CryptoNoteConfig.h checkpoint hash for height " + << checkpoint_height << " in chunk " << chunk_index + << " (overwrites DNS if present)"; + } + } + + return result; + } + + bool CheckpointList::validate_checkpoint_hash( + uint32_t checkpoint_height, + const crypto::Hash& expected_hash, + const std::vector& chunk_block_ids, + uint32_t index_in_chunk, + std::function(uint32_t startHeight, uint32_t maxCount)> getBlockIdsFunc) const + { + crypto::Hash actual_hash = NULL_HASH; + + // If getBlockIdsFunc is provided, re-fetch the original blockchain.dat hash for validation + // (This is needed because DNS may have already overwritten the value in chunk_block_ids) + if (getBlockIdsFunc) + { + std::vector original_block_ids = getBlockIdsFunc(checkpoint_height, 1); + if (!original_block_ids.empty()) + { + actual_hash = original_block_ids[0]; + } + } + else + { + // No re-fetch function provided - validate against current chunk_block_ids + // (This is used in validate_chunks_against_checkpoints where we already have the data) + if (index_in_chunk < chunk_block_ids.size()) + { + actual_hash = chunk_block_ids[index_in_chunk]; + } + } + + if (actual_hash == NULL_HASH) + { + logger(ERROR) << "Failed to get blockchain hash for checkpoint validation at height " << checkpoint_height; + return false; + } + + if (actual_hash != expected_hash) + { + logger(ERROR) << "Checkpoint validation failed at height " << checkpoint_height + << ": expected " << expected_hash + << ", got " << actual_hash; + return false; + } + + return true; + } + + /** + * Attempt to reach consensus on a chunk hash with peers + * + * Samples K peers with network diversity, collects their chunk hashes, + * and determines if M peers agree with the local hash. + * + * @param chunk_index The chunk index being validated + * @param local_chunk_hash The locally computed chunk hash + * @param getPeerChunkHashFunc Function to get chunk hash from a peer + * @param available_peers List of available peer IDs + * @param getPeerNetwork16Func Function to get /16 network for a peer + * @param req Consensus requirements (M, K, n) + * @param attempt_name Name of the attempt (for logging) + * @param total_null_hash_responses Reference to counter for NULL_HASH responses (updated) + * @param total_mismatches Reference to counter for mismatches (updated) + * @return AttemptResult with agreement count and consensus hash + */ + CheckpointList::AttemptResult CheckpointList::attempt_consensus_impl( + uint32_t chunk_index, + const crypto::Hash& local_chunk_hash, + std::function getPeerChunkHashFunc, + const std::vector& available_peers, + std::function getPeerNetwork16Func, + const ConsensusRequirements& req, + const std::string& attempt_name, + uint32_t& total_null_hash_responses, + uint32_t& total_mismatches) const + { + AttemptResult attempt_result; + attempt_result.agreements = 0; + attempt_result.consensus_hash = NULL_HASH; + + // Sample K peers (req.min_peers) with network diversity + PeerSamplingResult sampling_result = sample_peers_with_diversity( + available_peers, + getPeerNetwork16Func, + req.min_peers); + + const std::vector& sampled_peers = sampling_result.sampled_peers; + const std::map& network_votes = sampling_result.network_votes; + + // Check consensus: need M agreements from K sampled peers, with M agreements from at least n networks + // Track hash votes to find consensus hash (hash with M+ votes, if different from local) + // Use unordered_map since crypto::Hash doesn't have comparison operator for std::map + std::unordered_map> hash_votes; // hash -> vote count + // Track which networks agree with local hash (for network diversity requirement on M agreements) + std::map agreeing_networks_local; // network_16 -> count of agreeing peers + // Track which networks agree with consensus hash (if different from local) + // Use unordered_map since crypto::Hash doesn't have comparison operator for std::map + std::unordered_map, boost::hash> agreeing_networks_by_hash; // hash -> (network_16 -> count) + + uint32_t agreements = 0; + uint32_t null_hash_responses = 0; + uint32_t mismatches = 0; + + for (uint64_t peer_id : sampled_peers) + { + crypto::Hash peer_hash = getPeerChunkHashFunc(peer_id); + uint32_t net16 = getPeerNetwork16Func(peer_id); + + if (peer_hash == NULL_HASH) + { + // Peer didn't respond, timed out, or doesn't have this chunk in memory + // This is not necessarily a failure - the peer might not have created this chunk yet + // or might be using version 1 (doesn't support chunk-based checkpoints) + null_hash_responses++; + logger(INFO) << "Peer " << peer_id + << " returned NULL_HASH for chunk " << chunk_index + << " (" << attempt_name << ") - peer may not have this chunk in memory yet " + << "or may be using version 1 (doesn't support chunk checkpoints)"; + continue; // Don't count as agreement or disagreement - peer doesn't have the chunk + } + + // Count votes for this hash + hash_votes[peer_hash]++; + + if (peer_hash == local_chunk_hash) + { + agreements++; + agreeing_networks_local[net16]++; + logger(INFO) << "Peer " << peer_id << " agrees with local chunk " << chunk_index + << " hash (" << attempt_name << ") from network " << net16; + } + else + { + mismatches++; + agreeing_networks_by_hash[peer_hash][net16]++; + logger(WARNING) << "Peer " << peer_id + << " chunk hash mismatch for chunk " << chunk_index + << " (" << attempt_name << "): local=" << local_chunk_hash + << ", peer=" << peer_hash << " from network " << net16; + } + } + + // Find the consensus hash (hash with most votes, if >= M and different from local) + crypto::Hash consensus_hash = NULL_HASH; + uint32_t max_votes = 0; + for (const auto& vote : hash_votes) + { + if (vote.second >= req.min_agreements && vote.second > max_votes) + { + max_votes = vote.second; + consensus_hash = vote.first; + } + } + + // Only set consensus_hash if it's different from local and we have M+ agreements + if (consensus_hash != NULL_HASH && consensus_hash != local_chunk_hash && max_votes >= req.min_agreements) + { + attempt_result.consensus_hash = consensus_hash; + logger(WARNING) << "Chunk " << chunk_index << " (" << attempt_name + << "): M/K peers (" << max_votes << ") agree on different hash: " + << consensus_hash << " (local: " << local_chunk_hash << ")"; + } + + // CRITICAL: Verify that M agreeing peers come from at least n diverse networks + // This ensures consensus is not dominated by a single network + // Example: M=3, n=2 means we need 3 agreements from at least 2 different networks + // Valid: M11 M12 M21 (2 from network1, 1 from network2) or M11 M21 M22 (1 from network1, 2 from network2) + // Invalid: M11 M12 M13 (all 3 from network1 - not diverse enough) + uint32_t diverse_networks_in_agreements = 0; + bool meets_diversity_requirement = false; + + if (agreements >= req.min_agreements) + { + // Check if M agreeing peers come from at least n networks + diverse_networks_in_agreements = static_cast(agreeing_networks_local.size()); + meets_diversity_requirement = (diverse_networks_in_agreements >= req.min_diverse_networks); + + if (!meets_diversity_requirement) + { + logger(WARNING) << "Chunk " << chunk_index + << " consensus " << attempt_name << ": M=" << agreements + << " agreements but only from " << diverse_networks_in_agreements + << " network(s), need at least n=" << req.min_diverse_networks + << " networks for consensus"; + // Return 0 agreements if diversity requirement not met (consensus fails) + attempt_result.agreements = 0; + attempt_result.consensus_hash = NULL_HASH; + return attempt_result; + } + } + else if (consensus_hash != NULL_HASH && max_votes >= req.min_agreements) + { + // Check if M peers agreeing on consensus_hash come from at least n networks + auto it = agreeing_networks_by_hash.find(consensus_hash); + if (it != agreeing_networks_by_hash.end()) + { + diverse_networks_in_agreements = static_cast(it->second.size()); + meets_diversity_requirement = (diverse_networks_in_agreements >= req.min_diverse_networks); + + if (!meets_diversity_requirement) + { + logger(WARNING) << "Chunk " << chunk_index + << " consensus " << attempt_name << ": M=" << max_votes + << " peers agree on different hash but only from " << diverse_networks_in_agreements + << " network(s), need at least n=" << req.min_diverse_networks + << " networks for consensus"; + // Return 0 agreements if diversity requirement not met (consensus fails) + attempt_result.agreements = 0; + attempt_result.consensus_hash = NULL_HASH; + return attempt_result; + } + } + } + else + { + // Not enough agreements, but still track network diversity for logging + diverse_networks_in_agreements = static_cast(agreeing_networks_local.size()); + } + + // Also verify we sampled from at least n diverse networks (for K sampling requirement) + uint32_t diverse_networks_sampled = static_cast(network_votes.size()); + if (diverse_networks_sampled < req.min_diverse_networks) + { + logger(WARNING) << "Chunk " << chunk_index + << " consensus " << attempt_name << ": insufficient network diversity in sampling " + << "(sampled from " << diverse_networks_sampled << " networks, need n=" << req.min_diverse_networks << ")"; + // Return 0 agreements if diversity requirement not met (consensus fails) + attempt_result.agreements = 0; + attempt_result.consensus_hash = NULL_HASH; + return attempt_result; + } + + logger(INFO) << "Chunk " << chunk_index + << " consensus " << attempt_name << ": " << agreements + << "/" << sampled_peers.size() << " peers agreed (need M=" + << req.min_agreements << " from K=" << req.min_peers + << ", M agreements from n=" << diverse_networks_in_agreements + << " network(s), sampled from n=" << diverse_networks_sampled << " network(s))"; + + // Track statistics for better error messages + total_null_hash_responses += null_hash_responses; + total_mismatches += mismatches; + + attempt_result.agreements = agreements; + return attempt_result; + } + + /** + * Verify chunk with peer consensus (with second chance retry) + * + * This implements the "second chance" mechanism for noisy environments: + * 1. First attempt: Sample M peers from K, check if all agree with local hash + * 2. If mismatch: Re-sample a fresh M peers from K (second chance) + * 3. If still mismatch: Consensus failed + * + * This helps avoid rollback due to flaky peers in noisy network environments. + * + * @param chunk_index The chunk index to verify + * @param local_chunk_hash The locally computed chunk hash + * @param getPeerChunkHashFunc Function to get chunk hash from a peer + * @param available_peers List of available peer IDs that meet requirements + * @param getPeerNetwork16Func Function to get /16 network for a peer + * @return ConsensusResult with consensus status + */ + CheckpointList::ConsensusResult CheckpointList::verify_chunk_with_peer_consensus( + uint32_t chunk_index, + const crypto::Hash& local_chunk_hash, + std::function getPeerChunkHashFunc, + const std::vector& available_peers, + std::function getPeerNetwork16Func) const + { + ConsensusResult result; + result.consensus_reached = false; + result.agreements_first_attempt = 0; + result.agreements_second_attempt = 0; + result.used_second_chance = false; + result.consensus_hash_first_attempt = NULL_HASH; + result.consensus_hash_second_attempt = NULL_HASH; + + if (available_peers.empty()) + { + logger(WARNING) << "No available peers for chunk " << chunk_index << " consensus"; + return result; + } + + // Calculate consensus requirements + ConsensusRequirements req = calculate_consensus_requirements(available_peers.size()); + + // Ensure we have enough peers + if (available_peers.size() < req.min_peers) + { + logger(WARNING) << "Not enough peers for chunk " << chunk_index + << " consensus (have " << available_peers.size() + << ", need " << req.min_peers << ")"; + return result; + } + + // Helper variables to track statistics across both attempts + uint32_t total_null_hash_responses = 0; + uint32_t total_mismatches = 0; + + // First attempt + AttemptResult first_attempt = attempt_consensus_impl( + chunk_index, + local_chunk_hash, + getPeerChunkHashFunc, + available_peers, + getPeerNetwork16Func, + req, + "first attempt", + total_null_hash_responses, + total_mismatches); + result.agreements_first_attempt = first_attempt.agreements; + result.consensus_hash_first_attempt = first_attempt.consensus_hash; + + if (result.agreements_first_attempt >= req.min_agreements) + { + // Consensus reached on first attempt (peers agree with local hash) + result.consensus_reached = true; + logger(INFO) << "Chunk " << chunk_index + << " consensus reached on first attempt (" + << result.agreements_first_attempt << " agreements)"; + return result; + } + + // First attempt failed - use second chance + if (result.consensus_hash_first_attempt != NULL_HASH) + { + logger(WARNING) << "Chunk " << chunk_index + << " consensus failed on first attempt (" + << result.agreements_first_attempt << " agreements, need " + << req.min_agreements << "). " + << "M/K peers agree on different hash: " << result.consensus_hash_first_attempt + << " (local: " << local_chunk_hash << "). " + << "Re-sampling peers for second chance..."; + } + else + { + logger(WARNING) << "Chunk " << chunk_index + << " consensus failed on first attempt (" + << result.agreements_first_attempt << " agreements, need " + << req.min_agreements << "). Re-sampling peers for second chance..."; + } + + result.used_second_chance = true; + AttemptResult second_attempt = attempt_consensus_impl( + chunk_index, + local_chunk_hash, + getPeerChunkHashFunc, + available_peers, + getPeerNetwork16Func, + req, + "second attempt", + total_null_hash_responses, + total_mismatches); + result.agreements_second_attempt = second_attempt.agreements; + result.consensus_hash_second_attempt = second_attempt.consensus_hash; + + if (result.agreements_second_attempt >= req.min_agreements) + { + // Consensus reached on second attempt (peers agree with local hash) + result.consensus_reached = true; + logger(INFO) << "Chunk " << chunk_index + << " consensus reached on second attempt (" + << result.agreements_second_attempt << " agreements)"; + } + else + { + // Consensus failed even after second chance + // Check if we got the same consensus hash in both attempts (different from local) + // This indicates our blockchain diverged and we should rollback + if (result.consensus_hash_first_attempt != NULL_HASH && + result.consensus_hash_second_attempt != NULL_HASH && + result.consensus_hash_first_attempt == result.consensus_hash_second_attempt && + result.consensus_hash_first_attempt != local_chunk_hash) + { + // Same consensus hash in both attempts, different from local - ROLLBACK REQUIRED + logger(ERROR, BRIGHT_RED) << "Chunk " << chunk_index + << " ROLLBACK REQUIRED: M/K peers agree on different hash in BOTH attempts. " + << "Consensus hash: " << result.consensus_hash_first_attempt + << " (local: " << local_chunk_hash << "). " + << "This indicates blockchain divergence."; + } + else if (total_mismatches == 0 && total_null_hash_responses > 0) + { + // All peers returned NULL_HASH - this is expected if peers are v1 or don't have the chunk + logger(INFO) << "Chunk " << chunk_index + << " consensus not reached (all " << total_null_hash_responses + << " peer(s) returned NULL_HASH). This is normal if: " + << "(1) peers are using version 1 (don't support chunk checkpoints), or " + << "(2) peers haven't created this chunk yet. " + << "Chunk will remain in memory and validation will be retried when v2+ peers become available."; + } + else if (total_mismatches > 0) + { + // We got hash mismatches but not consistent consensus across both attempts + logger(WARNING) << "Chunk " << chunk_index + << " consensus failed after second attempt (first: " + << result.agreements_first_attempt << ", second: " + << result.agreements_second_attempt << ", need " + << req.min_agreements << "). " + << total_mismatches << " peer(s) returned different hash(es). " + << "Inconsistent results between attempts - monitoring for more mismatches."; + } + else + { + // No responses at all (shouldn't happen, but handle it) + logger(INFO) << "Chunk " << chunk_index + << " consensus not reached (no peer responses). " + << "Chunk will remain in memory and validation will be retried."; + } + } + + return result; + } +} diff --git a/src/CryptoNoteCore/Core.cpp b/src/CryptoNoteCore/Core.cpp index 99c3eb6a8..ee34f853c 100644 --- a/src/CryptoNoteCore/Core.cpp +++ b/src/CryptoNoteCore/Core.cpp @@ -60,20 +60,22 @@ class BlockWithTransactions : public IBlock { friend class core; }; -core::core(const Currency ¤cy, i_cryptonote_protocol *pprotocol, logging::ILogger &logger, bool blockchainIndexesEnabled, bool blockchainAutosaveEnabled) : m_currency(currency), - logger(logger, "core"), - m_mempool(currency, m_blockchain, m_timeProvider, logger), - m_blockchain(currency, m_mempool, logger, blockchainIndexesEnabled, blockchainAutosaveEnabled), - m_miner(new Miner(currency, *this, logger)), - m_starter_message_showed(false) +core::core(const Currency ¤cy, i_cryptonote_protocol *pprotocol, logging::ILogger &logger, bool blockchainIndexesEnabled, bool blockchainAutosaveEnabled) : + m_currency(currency), + logger(logger, "core"), + m_mempool(currency, m_blockchain, m_timeProvider, logger), + m_blockchain(currency, m_mempool, logger, blockchainIndexesEnabled, blockchainAutosaveEnabled), + m_miner(new Miner(currency, *this, logger)), + m_starter_message_showed(false) { set_cryptonote_protocol(pprotocol); m_blockchain.addObserver(this); m_mempool.addObserver(this); } - //----------------------------------------------------------------------------------------------- - core::~core() { + +//----------------------------------------------------------------------------------------------- +core::~core() { m_blockchain.removeObserver(this); } @@ -85,10 +87,6 @@ void core::set_cryptonote_protocol(i_cryptonote_protocol* pprotocol) { } } //----------------------------------------------------------------------------------- -void core::set_checkpoints(Checkpoints&& chk_pts) { - m_blockchain.setCheckpoints(std::move(chk_pts)); -} -//----------------------------------------------------------------------------------- void core::init_options(boost::program_options::options_description& /*desc*/) { } @@ -106,6 +104,10 @@ void core::get_blockchain_top(uint32_t& height, crypto::Hash& top_id) { top_id = m_blockchain.getTailId(height); } +crypto::Hash core::checkpoint_hash(uint32_t height) { + return m_blockchain.getCheckpointHash(height); +} + bool core::rollback_chain_to(uint32_t height) { return m_blockchain.rollbackBlockchainTo(height); } diff --git a/src/CryptoNoteCore/Core.h b/src/CryptoNoteCore/Core.h index 0be9257ff..40a619b4d 100644 --- a/src/CryptoNoteCore/Core.h +++ b/src/CryptoNoteCore/Core.h @@ -83,6 +83,10 @@ namespace cn { virtual bool addMessageQueue(MessageQueue& messageQueue) override; virtual bool removeMessageQueue(MessageQueue& messageQueue) override; + + virtual CheckpointList& getCheckpointList() override { + return m_blockchain.getCheckpointList(); + } uint32_t get_current_blockchain_height(); bool have_block(const crypto::Hash& id) override; @@ -93,7 +97,8 @@ namespace cn { virtual void get_blockchain_top(uint32_t& height, crypto::Hash& top_id) override; bool get_blocks(uint32_t start_offset, uint32_t count, std::list& blocks, std::list& txs); bool get_blocks(uint32_t start_offset, uint32_t count, std::list& blocks); - bool rollback_chain_to(uint32_t height); + bool rollback_chain_to(uint32_t height) override; + crypto::Hash checkpoint_hash(uint32_t height); template bool get_blocks(const t_ids_container& block_ids, t_blocks_container& blocks, t_missed_container& missed_bs) { @@ -101,6 +106,10 @@ namespace cn { } virtual bool queryBlocks(const std::vector& block_ids, uint64_t timestamp, uint32_t& start_height, uint32_t& current_height, uint32_t& full_offset, std::vector& entries) override; + + virtual std::vector getBlockIds(uint32_t start_height, uint32_t end_height) override { + return m_blockchain.getBlockIds(start_height, end_height); + } virtual bool queryBlocksLite(const std::vector& knownBlockIds, uint64_t timestamp, uint32_t& resStartHeight, uint32_t& resCurrentHeight, uint32_t& resFullOffset, std::vector& entries) override; virtual crypto::Hash getBlockIdByHeight(uint32_t height) override; @@ -117,7 +126,6 @@ namespace cn { uint64_t difficultyAtHeight(uint64_t height); void set_cryptonote_protocol(i_cryptonote_protocol *pprotocol); - void set_checkpoints(Checkpoints &&chk_pts); std::vector getPoolTransactions() override; bool getPoolTransaction(const crypto::Hash &tx_hash, Transaction &transaction) override; @@ -160,7 +168,7 @@ namespace cn { bool add_new_tx(const Transaction &tx, const crypto::Hash &tx_hash, size_t blob_size, tx_verification_context &tvc, bool keeped_by_block, uint32_t height); bool load_state_data(); bool parse_tx_from_blob(Transaction &tx, crypto::Hash &tx_hash, crypto::Hash &tx_prefix_hash, const BinaryArray &blob); - bool handle_incoming_block(const Block &b, block_verification_context &bvc, bool control_miner, bool relay_block); + bool handle_incoming_block(const Block &b, block_verification_context &bvc, bool control_miner, bool relay_block) override; bool check_tx_syntax(const Transaction &tx); //check correct values, amounts and all lightweight checks not related with database diff --git a/src/CryptoNoteCore/Currency.cpp b/src/CryptoNoteCore/Currency.cpp index b493e9821..111d5dff6 100644 --- a/src/CryptoNoteCore/Currency.cpp +++ b/src/CryptoNoteCore/Currency.cpp @@ -482,14 +482,32 @@ namespace cn if (amount_out > amount_in) { - // interest shows up in the output of the W/D transactions and W/Ds always have min fee - if (tx.inputs.size() > 0 && !tx.outputs.empty() && amount_out > amount_in + parameters::MINIMUM_FEE) + // Check if this is actually a deposit multisig spend (withdrawal) + bool isDepositWithdrawal = false; + for (const auto &in : tx.inputs) { + if (in.type() == typeid(MultisignatureInput)) + { + const auto &msig = boost::get(in); + if (msig.term != 0) + { + isDepositWithdrawal = true; + break; + } + } + } + + if (isDepositWithdrawal) + { + // Interest shows up in the output of withdrawal transactions + // W/Ds always have min fee fee = parameters::MINIMUM_FEE; - logger(INFO) << "TRIGGERED: Currency.cpp getTransactionFee"; + return true; } else { + // Not a withdrawal — overspend is invalid + logger(ERROR) << "Transaction has outputs > inputs but is not a deposit withdrawal"; return false; } } @@ -1392,6 +1410,7 @@ namespace cn blockIndexesFileName(parameters::CRYPTONOTE_BLOCKINDEXES_FILENAME); txPoolFileName(parameters::CRYPTONOTE_POOLDATA_FILENAME); blockchinIndicesFileName(parameters::CRYPTONOTE_BLOCKCHAIN_INDICES_FILENAME); + checkpointFileName(parameters::CRYPTONOTE_CHECKPOINT_FILENAME); testnet(false); } diff --git a/src/CryptoNoteCore/Currency.h b/src/CryptoNoteCore/Currency.h index a81f91c43..d70b1d402 100644 --- a/src/CryptoNoteCore/Currency.h +++ b/src/CryptoNoteCore/Currency.h @@ -148,6 +148,7 @@ namespace cn const std::string &blockIndexesFileName() const { return m_blockIndexesFileName; } const std::string &txPoolFileName() const { return m_txPoolFileName; } const std::string &blockchinIndicesFileName() const { return m_blockchinIndicesFileName; } + const std::string &checkpointFileName() const { return m_checkpointFileName; } bool isTestnet() const { return m_testnet; } @@ -290,6 +291,7 @@ namespace cn std::string m_blockIndexesFileName; std::string m_txPoolFileName; std::string m_blockchinIndicesFileName; + std::string m_checkpointFileName; static const std::vector REWARD_INCREASING_FACTOR; @@ -634,6 +636,11 @@ namespace cn m_currency.m_blockchinIndicesFileName = val; return *this; } + CurrencyBuilder &checkpointFileName(const std::string &val) + { + m_currency.m_checkpointFileName = val; + return *this; + } CurrencyBuilder &genesisCoinbaseTxHex(const std::string &val) { @@ -694,4 +701,4 @@ namespace cn } }; -} // namespace cn \ No newline at end of file +} // namespace cn diff --git a/src/CryptoNoteCore/ICore.h b/src/CryptoNoteCore/ICore.h index 91b883c14..c6ee56436 100644 --- a/src/CryptoNoteCore/ICore.h +++ b/src/CryptoNoteCore/ICore.h @@ -27,6 +27,7 @@ struct COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS_response; struct NOTIFY_RESPONSE_GET_OBJECTS_request; struct NOTIFY_REQUEST_GET_OBJECTS_request; +class CheckpointList; class Currency; class IBlock; class ICoreObserver; @@ -65,6 +66,7 @@ class ICore { virtual void on_synchronized() = 0; virtual size_t addChain(const std::vector& chain) = 0; + virtual CheckpointList& getCheckpointList() = 0; virtual void get_blockchain_top(uint32_t& height, crypto::Hash& top_id) = 0; virtual std::vector findBlockchainSupplement(const std::vector& remoteBlockIds, size_t maxCount, uint32_t& totalBlockCount, uint32_t& startBlockIndex) = 0; @@ -83,6 +85,7 @@ class ICore { std::vector& deletedTxsIds) = 0; virtual bool queryBlocks(const std::vector& block_ids, uint64_t timestamp, uint32_t& start_height, uint32_t& current_height, uint32_t& full_offset, std::vector& entries) = 0; + virtual std::vector getBlockIds(uint32_t start_height, uint32_t end_height) = 0; virtual bool queryBlocksLite(const std::vector& block_ids, uint64_t timestamp, uint32_t& start_height, uint32_t& current_height, uint32_t& full_offset, std::vector& entries) = 0; @@ -114,6 +117,7 @@ class ICore { virtual bool addMessageQueue(MessageQueue& messageQueue) = 0; virtual bool removeMessageQueue(MessageQueue& messageQueue) = 0; + virtual bool rollback_chain_to(uint32_t height) = 0; }; } //namespace cn diff --git a/src/CryptoNoteCore/TransactionExtra.cpp b/src/CryptoNoteCore/TransactionExtra.cpp index 9f8118139..4510acab5 100644 --- a/src/CryptoNoteCore/TransactionExtra.cpp +++ b/src/CryptoNoteCore/TransactionExtra.cpp @@ -310,7 +310,22 @@ namespace cn bool parsePaymentId(const std::string &paymentIdString, Hash &paymentId) { - return common::podFromHex(paymentIdString, paymentId); + // If input looks like transaction extra data (35 bytes) + if (paymentIdString.length() == 35) { + std::vector extraNonce; + extraNonce.resize(paymentIdString.length() - 2); // Skip TX_EXTRA_NONCE tag + memcpy(extraNonce.data(), paymentIdString.data() + 2, paymentIdString.length() - 2); + + if (getPaymentIdFromTransactionExtraNonce(extraNonce, paymentId)) { + return true; + } + } + // Try parsing as hex string + if (paymentIdString.length() == 64) { + return common::podFromHex(paymentIdString, paymentId); + } + + return false; } bool createTxExtraWithPaymentId(const std::string &paymentIdString, std::vector &extra) diff --git a/src/CryptoNoteProtocol/CryptoNoteProtocolDefinitions.h b/src/CryptoNoteProtocol/CryptoNoteProtocolDefinitions.h index b9f626115..e3411f543 100644 --- a/src/CryptoNoteProtocol/CryptoNoteProtocolDefinitions.h +++ b/src/CryptoNoteProtocol/CryptoNoteProtocolDefinitions.h @@ -225,6 +225,9 @@ namespace cn typedef NOTIFY_NEW_LITE_BLOCK_request request; }; + /************************************************************************/ + /* */ + /************************************************************************/ struct NOTIFY_MISSING_TXS_request { crypto::Hash blockHash; @@ -244,5 +247,81 @@ namespace cn const static int ID = BC_COMMANDS_POOL_BASE + 10; typedef NOTIFY_MISSING_TXS_request request; }; + + /************************************************************************/ + /* */ + /************************************************************************/ + struct NOTIFY_REQUEST_CHECKPOINT_LIST_request + { + crypto::Hash target_hash; + uint32_t start_height; + uint32_t end_height; + + void serialize(ISerializer &s) + { + KV_MEMBER(target_hash) + KV_MEMBER(start_height) + KV_MEMBER(end_height) + } + }; + + struct NOTIFY_REQUEST_CHECKPOINT_LIST + { + const static int ID = BC_COMMANDS_POOL_BASE + 11; + typedef NOTIFY_REQUEST_CHECKPOINT_LIST_request request; + }; + + struct NOTIFY_RESPONSE_CHECKPOINT_LIST_request + { + uint32_t list_start_height; + std::vector checkpoint_list; + + void serialize(ISerializer &s) + { + KV_MEMBER(list_start_height) + serializeAsBinary(checkpoint_list, "checkpoint_list", s); + } + }; + + struct NOTIFY_RESPONSE_CHECKPOINT_LIST + { + const static int ID = BC_COMMANDS_POOL_BASE + 12; + typedef NOTIFY_RESPONSE_CHECKPOINT_LIST_request request; + }; + + // New protocol for chunk hash requests (version 2 checkpoint system) + struct NOTIFY_REQUEST_CHUNK_HASH_request + { + uint32_t chunk_index; // Which chunk to request (0-based) + + void serialize(ISerializer &s) + { + KV_MEMBER(chunk_index) + } + }; + + struct NOTIFY_REQUEST_CHUNK_HASH + { + const static int ID = BC_COMMANDS_POOL_BASE + 13; + typedef NOTIFY_REQUEST_CHUNK_HASH_request request; + }; + + struct NOTIFY_RESPONSE_CHUNK_HASH_request + { + uint32_t chunk_index; + crypto::Hash chunk_hash; // The requested chunk hash, or NULL_HASH if chunk doesn't exist + + void serialize(ISerializer &s) + { + KV_MEMBER(chunk_index) + KV_MEMBER(chunk_hash) + } + }; + + struct NOTIFY_RESPONSE_CHUNK_HASH + { + const static int ID = BC_COMMANDS_POOL_BASE + 14; + typedef NOTIFY_RESPONSE_CHUNK_HASH_request request; + }; } // namespace cn diff --git a/src/CryptoNoteProtocol/CryptoNoteProtocolHandler.cpp b/src/CryptoNoteProtocol/CryptoNoteProtocolHandler.cpp index 9c9ae7abb..cff430a55 100644 --- a/src/CryptoNoteProtocol/CryptoNoteProtocolHandler.cpp +++ b/src/CryptoNoteProtocol/CryptoNoteProtocolHandler.cpp @@ -10,6 +10,17 @@ #include "CryptoNoteProtocolHandler.h" #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -21,6 +32,8 @@ #include "CryptoNoteCore/VerificationContext.h" #include "P2p/LevinProtocol.h" #include // for UINT64_MAX +#include "CryptoNoteCore/CheckpointList.h" +#include "CryptoNoteProtocol/CryptoNoteProtocolHandlerChunk.h" using namespace logging; using namespace common; @@ -51,6 +64,7 @@ CryptoNoteProtocolHandler::CryptoNoteProtocolHandler(const Currency ¤cy, p m_core(rcore), m_synchronized(false), m_stop(false), + m_last_checkpoint_req(0), m_observedHeight(0), m_peersCount(0), logger(log, "protocol"), @@ -59,6 +73,10 @@ CryptoNoteProtocolHandler::CryptoNoteProtocolHandler(const Currency ¤cy, p { if (!m_p2p) m_p2p = &m_p2p_stub; + + // Initialize chunk validation manager + m_chunkValidationManager = std::unique_ptr( + new ChunkValidationManager(m_core, m_p2p, m_currency, log, m_peersCount, m_stop)); } size_t CryptoNoteProtocolHandler::getPeerCount() const @@ -72,6 +90,12 @@ void CryptoNoteProtocolHandler::set_p2p_endpoint(IP2pEndpoint *p2p) m_p2p = p2p; else m_p2p = &m_p2p_stub; + + // Update chunk validation manager with the new P2P endpoint + if (m_chunkValidationManager) + { + m_chunkValidationManager->set_p2p_endpoint(m_p2p); + } } void CryptoNoteProtocolHandler::onConnectionOpened(CryptoNoteConnectionContext &context) @@ -196,10 +220,10 @@ bool CryptoNoteProtocolHandler::process_payload_sync_data(const CORE_SYNC_DATA & { int64_t diff = static_cast(hshd.current_height) - static_cast(get_current_blockchain_height()); - logger(diff >= 0 ? (is_inital ? logging::INFO : DEBUGGING) : logging::TRACE) << context << "Unknown top block: " << get_current_blockchain_height() << " -> " << hshd.current_height - << std::endl - - << "Synchronization started"; + logger(diff >= 0 ? (is_inital ? logging::INFO : DEBUGGING) : logging::TRACE) << context + << "Unknown top block: " << get_current_blockchain_height() << " -> " << hshd.current_height + << std::endl + << "Synchronization started"; logger(DEBUGGING) << "Remote top block height: " << hshd.current_height << ", id: " << hshd.top_id; //let the socket to send response to handshake, but request callback, to let send request data after response @@ -216,6 +240,34 @@ bool CryptoNoteProtocolHandler::process_payload_sync_data(const CORE_SYNC_DATA & m_observerManager.notify(&ICryptoNoteProtocolObserver::peerCountUpdated, m_peersCount.load()); } + if (context.version < cn::P2P_CHECKPOINT_LIST_VERSION || context.m_checkpoints_not_in_sync || context.m_active_checkpoint_req) + return true; + + // Only request checkpoints if we can answer them (have confirmed chunks) + // This prevents nodes without confirmed chunks from requesting from peers that also can't answer + if (!m_core.getCheckpointList().can_answer_checkpoint_requests()) + return true; + + uint64_t time_now = time(nullptr); + uint64_t last_request = time_now - m_last_checkpoint_req.load(); + if (last_request > P2P_CHECKPOINT_LIST_RE_REQUEST) + return true; + + auto cl_status = m_core.getCheckpointList().get_incomplete_checkpoint_target(); + if ( cl_status.target_hash != NULL_HASH ) + { + NOTIFY_REQUEST_CHECKPOINT_LIST::request req = boost::value_initialized(); + req.target_hash = cl_status.target_hash; + req.start_height = cl_status.start_height; + req.end_height = cl_status.end_height; + + if (!post_notify(*m_p2p, req, context)) + return false; + + m_last_checkpoint_req = time_now; + context.m_active_checkpoint_req = true; + } + return true; } @@ -256,12 +308,25 @@ int CryptoNoteProtocolHandler::handleCommand(bool is_notify, int command, const int ret = 0; handled = true; + // Debug logging for chunk hash messages (BC_COMMANDS_POOL_BASE + 13 = REQUEST, + 14 = RESPONSE) + const int CHUNK_HASH_REQUEST_ID = BC_COMMANDS_POOL_BASE + 13; + const int CHUNK_HASH_RESPONSE_ID = BC_COMMANDS_POOL_BASE + 14; + if (command == CHUNK_HASH_REQUEST_ID || command == CHUNK_HASH_RESPONSE_ID) + { + logger(INFO) << ctx << " handleCommand: received " << (is_notify ? "NOTIFY" : "REQUEST") + << " command " << command << " (chunk hash message)"; + } + switch (command) { HANDLE_NOTIFY(NOTIFY_NEW_BLOCK, &CryptoNoteProtocolHandler::handle_notify_new_block) HANDLE_NOTIFY(NOTIFY_NEW_TRANSACTIONS, &CryptoNoteProtocolHandler::handle_notify_new_transactions) HANDLE_NOTIFY(NOTIFY_REQUEST_GET_OBJECTS, &CryptoNoteProtocolHandler::handle_request_get_objects) HANDLE_NOTIFY(NOTIFY_RESPONSE_GET_OBJECTS, &CryptoNoteProtocolHandler::handle_response_get_objects) + HANDLE_NOTIFY(NOTIFY_REQUEST_CHECKPOINT_LIST, &CryptoNoteProtocolHandler::handle_request_checkpoint_list) + HANDLE_NOTIFY(NOTIFY_RESPONSE_CHECKPOINT_LIST, &CryptoNoteProtocolHandler::handle_response_checkpoint_list) + HANDLE_NOTIFY(NOTIFY_REQUEST_CHUNK_HASH, &CryptoNoteProtocolHandler::handle_request_chunk_hash) + HANDLE_NOTIFY(NOTIFY_RESPONSE_CHUNK_HASH, &CryptoNoteProtocolHandler::handle_response_chunk_hash) HANDLE_NOTIFY(NOTIFY_REQUEST_CHAIN, &CryptoNoteProtocolHandler::handle_request_chain) HANDLE_NOTIFY(NOTIFY_RESPONSE_CHAIN_ENTRY, &CryptoNoteProtocolHandler::handle_response_chain_entry) HANDLE_NOTIFY(NOTIFY_REQUEST_TX_POOL, &CryptoNoteProtocolHandler::handle_request_tx_pool) @@ -270,6 +335,14 @@ int CryptoNoteProtocolHandler::handleCommand(bool is_notify, int command, const default: handled = false; + // Debug logging for unhandled commands (especially chunk hash related) + const int CHUNK_HASH_REQUEST_ID = BC_COMMANDS_POOL_BASE + 13; + const int CHUNK_HASH_RESPONSE_ID = BC_COMMANDS_POOL_BASE + 14; + if (command == CHUNK_HASH_REQUEST_ID || command == CHUNK_HASH_RESPONSE_ID) + { + logger(ERROR) << ctx << " handleCommand: chunk hash command " << command + << " not handled! This should not happen."; + } } return ret; @@ -398,14 +471,14 @@ int CryptoNoteProtocolHandler::handle_request_get_objects(int command, NOTIFY_RE size_t maxObjects = m_maxObjectCount.load(); const size_t totalObjects = arg.blocks.size() + arg.txs.size(); - logger(INFO) << "DEBUG: Request for " << totalObjects << " objects (limit: " << maxObjects << ")"; - - if (totalObjects > maxObjects) { + std::string ipAddress = common::ipAddressToString(context.m_remote_ip); logger(logging::ERROR) << context << "Requested objects count exceeds limit of " << maxObjects << ": blocks " << arg.blocks.size() << " + txs " << arg.txs.size() << " = " << totalObjects; + logger(logging::WARNING) << "IP " << ipAddress << ":" << context.m_remote_port + << " attempted to overload with " << totalObjects << " objects (limit: " << maxObjects << ")"; context.m_state = CryptoNoteConnectionContext::state_shutdown; return 1; } @@ -610,6 +683,21 @@ int CryptoNoteProtocolHandler::processObjects(CryptoNoteConnectionContext& conte bool CryptoNoteProtocolHandler::on_idle() { + // Periodically validate unverified chunks and prefetch future chunk hashes. + // Only for version 2+ nodes (chunked checkpoint system). + if (cn::P2P_CURRENT_VERSION >= cn::P2P_CHECKPOINT_LIST_VERSION) + { + m_chunkValidationManager->check_pending_chunk_validations(); + m_chunkValidationManager->validate_unverified_chunks(); + + // Forward prefetch: fetch chunk hashes the network already has but we don't yet. + // This extends the checkpoint zone so IBD stays at fast speed. + // Observed height is local_tip + 1 per peer convention; subtract 1 for actual tip. + uint32_t obs = getObservedHeight(); + uint32_t net_tip = (obs > 0) ? (obs - 1) : 0; + m_chunkValidationManager->prefetch_missing_chunks(net_tip); + } + return m_core.on_idle(); } @@ -1132,4 +1220,209 @@ int CryptoNoteProtocolHandler::doPushLiteBlock(NOTIFY_NEW_LITE_BLOCK::request ar return 1; } +int CryptoNoteProtocolHandler::handle_request_checkpoint_list(int command, NOTIFY_REQUEST_CHECKPOINT_LIST::request& arg, CryptoNoteConnectionContext& context) +{ + // REFACTORED: This handler now supports both old (full checkpoint lists) and new (chunk-based) systems + // For backward compatibility with old nodes, we still support full checkpoint list requests + // However, new nodes should use NOTIFY_REQUEST_CHUNK_HASH for consensus + + // Only answer checkpoint requests if we have confirmed chunks + // This ensures we don't serve unverified data to other nodes + if (!m_core.getCheckpointList().can_answer_checkpoint_requests()) + { + logger(logging::DEBUGGING) << context << " Cannot answer checkpoint request: no confirmed chunks available"; + NOTIFY_RESPONSE_CHECKPOINT_LIST::request rsp; + rsp.list_start_height = 0; + rsp.checkpoint_list.clear(); + if (!post_notify(*m_p2p, rsp, context)) + context.m_state = CryptoNoteConnectionContext::state_shutdown; + return 1; + } + + // REFACTORED: For chunk-based system, we can still respond with block IDs if requested + // but this is mainly for backward compatibility with old nodes + // New nodes should use chunk hash requests instead + NOTIFY_RESPONSE_CHECKPOINT_LIST::request rsp; + rsp.list_start_height = arg.start_height; + rsp.checkpoint_list = m_core.getBlockIds(arg.start_height, arg.end_height); + + crypto::Hash hv = crypto::cn_fast_hash(rsp.checkpoint_list.data(), rsp.checkpoint_list.size() * sizeof(crypto::Hash)); + if ( hv != arg.target_hash || rsp.checkpoint_list.size() == 0) + { + logger(ERROR) << context << " peer requested different hash for start_height " << arg.start_height + << " end_height " << arg.end_height << " checkpoint list size " << rsp.checkpoint_list.size() + << " I have hash " << hv << " peer wants " << arg.target_hash; + + rsp.checkpoint_list.clear(); + rsp.list_start_height = 0; + context.m_checkpoints_not_in_sync = true; + } + + if (!post_notify(*m_p2p, rsp, context)) + context.m_state = CryptoNoteConnectionContext::state_shutdown; + return 1; +} + +int CryptoNoteProtocolHandler::handle_request_chunk_hash(int command, NOTIFY_REQUEST_CHUNK_HASH::request& arg, CryptoNoteConnectionContext& context) +{ + logger(INFO) << context << " Received chunk hash request for chunk " << arg.chunk_index; + + // Only answer chunk hash requests if peer supports chunk-based checkpoints + if (context.version < cn::P2P_CHECKPOINT_LIST_VERSION) + { + logger(INFO) << context << " Peer doesn't support chunk-based checkpoints (version " << context.version << ")"; + NOTIFY_RESPONSE_CHUNK_HASH::request rsp; + rsp.chunk_index = arg.chunk_index; + rsp.chunk_hash = NULL_HASH; // Signal that we can't answer + if (!post_notify(*m_p2p, rsp, context)) + context.m_state = CryptoNoteConnectionContext::state_shutdown; + return 1; + } + + // Only answer chunk hash requests after this node has at least one confirmed chunk. + // Once bootstrapped, locally computed in-memory chunks may be served so peers can + // compare them during their own consensus process. + if (!m_core.getCheckpointList().can_answer_checkpoint_requests()) + { + logger(INFO) << context << " Cannot answer chunk hash request: no confirmed chunks available"; + NOTIFY_RESPONSE_CHUNK_HASH::request rsp; + rsp.chunk_index = arg.chunk_index; + rsp.chunk_hash = NULL_HASH; // Signal that we can't answer + if (!post_notify(*m_p2p, rsp, context)) + context.m_state = CryptoNoteConnectionContext::state_shutdown; + return 1; + } + + // Return chunks that exist in memory, including not-yet-confirmed chunks. + // The requester treats this as one peer vote and validates it against consensus. + crypto::Hash chunk_hash = m_core.getCheckpointList().get_chunk_hash(arg.chunk_index); + + NOTIFY_RESPONSE_CHUNK_HASH::request rsp; + rsp.chunk_index = arg.chunk_index; + rsp.chunk_hash = chunk_hash; + + if (chunk_hash == NULL_HASH) + { + logger(INFO) << context << " Cannot answer chunk hash request for chunk " << arg.chunk_index + << ": chunk not found in memory (have " + << m_core.getCheckpointList().get_all_chunk_hashes().size() << " chunks)"; + } + else + { + logger(INFO) << context << " Responding to chunk hash request for chunk " << arg.chunk_index + << " with hash " << chunk_hash; + } + + // Always send a response (even if NULL_HASH) so the requester knows we processed the request + logger(INFO) << context << " Sending chunk hash response for chunk " << arg.chunk_index + << " (hash: " << (chunk_hash == NULL_HASH ? "NULL_HASH" : "valid") << ")"; + if (!post_notify(*m_p2p, rsp, context)) + { + logger(ERROR) << context << " Failed to send chunk hash response for chunk " << arg.chunk_index; + context.m_state = CryptoNoteConnectionContext::state_shutdown; + } + else + { + logger(INFO) << context << " Successfully sent chunk hash response for chunk " << arg.chunk_index; + } + return 1; +} + +int CryptoNoteProtocolHandler::handle_response_chunk_hash(int command, NOTIFY_RESPONSE_CHUNK_HASH::request& arg, CryptoNoteConnectionContext& context) +{ + // Only process chunk hash responses if peer supports chunk-based checkpoints + if (context.version < cn::P2P_CHECKPOINT_LIST_VERSION) + { + logger(INFO) << context << " Peer doesn't support chunk-based checkpoints (version " << context.version << ")"; + return 1; + } + + // Find peer_id from context using connection_id (more reliable than pointer comparison) + uint64_t peer_id = 0; + m_p2p->for_each_connection([&peer_id, &context](const CryptoNoteConnectionContext& ctx, PeerIdType id) { + if (ctx.m_connection_id == context.m_connection_id) { + peer_id = id; + } + }); + + if (peer_id == 0) { + logger(WARNING) << context << " Cannot find peer ID for chunk hash response (connection_id: " + << context.m_connection_id << ")"; + return 1; + } + + // Store the response in pending chunk hashes map for the consensus mechanism + // NOTE: NULL_HASH is a valid response (means peer doesn't have this chunk) + if (!m_chunkValidationManager->store_chunk_hash_response(peer_id, arg.chunk_index, arg.chunk_hash)) + { + logger(WARNING) << context << " Ignored chunk hash response for chunk " << arg.chunk_index + << " from peer " << peer_id + << " (not pending, late, or peer was not requested)"; + return 1; + } + + if (arg.chunk_hash == NULL_HASH) + { + logger(INFO) << context << " Received chunk " << arg.chunk_index << " hash from peer " << peer_id + << ": peer does not have this chunk (NULL_HASH)"; + } + else + { + logger(INFO) << context << " Received chunk " << arg.chunk_index << " hash from peer " << peer_id + << " with hash " << arg.chunk_hash; + } + return 1; +} + +crypto::Hash CryptoNoteProtocolHandler::request_chunk_hash_from_peer(uint64_t peer_id, uint32_t chunk_index) +{ + // Find the peer connection + CryptoNoteConnectionContext* peer_context = nullptr; + uint32_t found_connections = 0; + m_p2p->for_each_connection([&peer_context, &found_connections, peer_id](CryptoNoteConnectionContext& ctx, uint64_t id) { + found_connections++; + if (id == peer_id) { + peer_context = &ctx; + } + }); + + if (!peer_context) { + logger(WARNING) << "Cannot request chunk hash from peer " << peer_id << ": peer not found (searched " + << found_connections << " connections)"; + // Log all available peer IDs for debugging + logger(DEBUGGING) << "Available peer IDs:"; + m_p2p->for_each_connection([&](CryptoNoteConnectionContext& ctx, uint64_t id) { + logger(DEBUGGING) << " Peer ID: " << id << " " << ctx + << ", state: " << get_protocol_state_string(ctx.m_state) + << ", version: " << static_cast(ctx.version); + }); + return NULL_HASH; + } + + // NOTE: This function is deprecated - we now use async validation via ChunkValidationManager + // This synchronous version is kept for backward compatibility but should not be used + // Responses will be handled asynchronously by the ChunkValidationManager + logger(WARNING) << "request_chunk_hash_from_peer is deprecated - use async validation instead"; + return NULL_HASH; +} + +int CryptoNoteProtocolHandler::handle_response_checkpoint_list(int command, NOTIFY_RESPONSE_CHECKPOINT_LIST::request& arg, CryptoNoteConnectionContext& context) +{ + if( !context.m_active_checkpoint_req ) + { + context.m_state = CryptoNoteConnectionContext::state_shutdown; + return 1; + } + + m_last_checkpoint_req = 0; + context.m_active_checkpoint_req = false; + + if( arg.checkpoint_list.size() == 0 || !m_core.getCheckpointList().add_checkpoint_list(arg.list_start_height, arg.checkpoint_list) ) + { + context.m_checkpoints_not_in_sync = true; + } + + return 1; +} + }; // namespace cn diff --git a/src/CryptoNoteProtocol/CryptoNoteProtocolHandler.h b/src/CryptoNoteProtocol/CryptoNoteProtocolHandler.h index e990ac30d..d2450d9c9 100644 --- a/src/CryptoNoteProtocol/CryptoNoteProtocolHandler.h +++ b/src/CryptoNoteProtocol/CryptoNoteProtocolHandler.h @@ -9,6 +9,7 @@ #include #include +#include #include #include "../CryptoNoteConfig.h" @@ -18,6 +19,7 @@ #include "CryptoNoteProtocol/CryptoNoteProtocolHandlerCommon.h" #include "CryptoNoteProtocol/ICryptoNoteProtocolObserver.h" #include "CryptoNoteProtocol/ICryptoNoteProtocolQuery.h" +#include "CryptoNoteProtocol/CryptoNoteProtocolHandlerChunk.h" #include "P2p/P2pProtocolDefinitions.h" #include "P2p/NetNodeCommon.h" @@ -25,6 +27,7 @@ #include + namespace platform_system { class Dispatcher; } @@ -81,6 +84,16 @@ namespace cn int handle_notify_new_transactions(int command, NOTIFY_NEW_TRANSACTIONS::request& arg, CryptoNoteConnectionContext& context); int handle_request_get_objects(int command, NOTIFY_REQUEST_GET_OBJECTS::request& arg, CryptoNoteConnectionContext& context); int handle_response_get_objects(int command, NOTIFY_RESPONSE_GET_OBJECTS::request& arg, CryptoNoteConnectionContext& context); + int handle_request_checkpoint_list(int command, NOTIFY_REQUEST_CHECKPOINT_LIST::request& arg, CryptoNoteConnectionContext& context); + int handle_response_checkpoint_list(int command, NOTIFY_RESPONSE_CHECKPOINT_LIST::request& arg, CryptoNoteConnectionContext& context); + int handle_request_chunk_hash(int command, NOTIFY_REQUEST_CHUNK_HASH::request& arg, CryptoNoteConnectionContext& context); + int handle_response_chunk_hash(int command, NOTIFY_RESPONSE_CHUNK_HASH::request& arg, CryptoNoteConnectionContext& context); + + // Request chunk hash from a specific peer (for consensus mechanism) + // @param peer_id The peer ID to request from + // @param chunk_index The chunk index to request + // @return The chunk hash, or NULL_HASH if request failed or peer doesn't have it + crypto::Hash request_chunk_hash_from_peer(uint64_t peer_id, uint32_t chunk_index); int handle_request_chain(int command, NOTIFY_REQUEST_CHAIN::request& arg, CryptoNoteConnectionContext& context); int handle_response_chain_entry(int command, NOTIFY_RESPONSE_CHAIN_ENTRY::request& arg, CryptoNoteConnectionContext& context); int handle_request_tx_pool(int command, NOTIFY_REQUEST_TX_POOL::request &arg, CryptoNoteConnectionContext &context); @@ -112,7 +125,9 @@ namespace cn IP2pEndpoint* m_p2p; std::atomic m_synchronized; std::atomic m_stop; - std::recursive_mutex m_sync_lock; + std::recursive_mutex m_sync_lock; + + std::atomic m_last_checkpoint_req; mutable std::mutex m_observedHeightMutex; uint32_t m_observedHeight; @@ -121,5 +136,8 @@ namespace cn tools::ObserverManager m_observerManager; std::atomic m_maxObjectCount; + + // Chunk validation manager (handles asynchronous P2P consensus) + std::unique_ptr m_chunkValidationManager; }; } diff --git a/src/CryptoNoteProtocol/CryptoNoteProtocolHandlerChunk.cpp b/src/CryptoNoteProtocol/CryptoNoteProtocolHandlerChunk.cpp new file mode 100644 index 000000000..b0f6d734f --- /dev/null +++ b/src/CryptoNoteProtocol/CryptoNoteProtocolHandlerChunk.cpp @@ -0,0 +1,923 @@ +// Copyright (c) 2011-2017 The Cryptonote developers +// Copyright (c) 2017-2018 The Circle Foundation & Conceal Devs +// Copyright (c) 2018-2026 Conceal Network & Conceal Devs +// +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "CryptoNoteProtocolHandlerChunk.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "CryptoNoteCore/CheckpointList.h" +#include "CryptoNoteCore/Currency.h" +#include "CryptoNoteProtocol/CryptoNoteProtocolDefinitions.h" +#include "CryptoNoteProtocol/CryptoNoteProtocolHandler.h" +#include "P2p/ConnectionContext.h" +#include "P2p/LevinProtocol.h" + +using namespace logging; +using namespace common; + +namespace cn +{ + +namespace +{ + // Helper template function to send notifications (same as in CryptoNoteProtocolHandler.cpp) + template + bool post_notify(IP2pEndpoint &p2p, typename t_parametr::request &arg, const CryptoNoteConnectionContext &context) + { + return p2p.invoke_notify_to_peer(t_parametr::ID, LevinProtocol::encode(arg), context); + } +} // namespace + +ChunkValidationManager::ChunkValidationManager(ICore& core, + IP2pEndpoint* p2p, + const Currency& currency, + logging::ILogger& log, + std::atomic& peersCount, + std::atomic& stop) + : m_core(core) + , m_p2p(p2p) + , m_currency(currency) + , logger(log, "chunk_validation") + , m_peersCount(peersCount) + , m_stop(stop) + , m_current_validating_chunk_index(UINT32_MAX) + , m_last_chunk_validation_attempt(0) + , m_last_prefetch_attempt(0) +{ +} + +void ChunkValidationManager::set_p2p_endpoint(IP2pEndpoint* p2p) +{ + if (p2p) + { + m_p2p = p2p; + logger(INFO) << "Updated P2P endpoint in ChunkValidationManager"; + } + else + { + logger(WARNING) << "Attempted to set null P2P endpoint in ChunkValidationManager"; + } +} + +bool ChunkValidationManager::send_chunk_hash_request_async(uint64_t peer_id, uint32_t chunk_index) +{ + // Find the peer connection + CryptoNoteConnectionContext* peer_context = nullptr; + m_p2p->for_each_connection([&peer_context, peer_id](CryptoNoteConnectionContext& ctx, uint64_t id) { + if (id == peer_id) { + peer_context = &ctx; + } + }); + + if (!peer_context) { + logger(WARNING) << "Cannot send async chunk hash request to peer " << peer_id << ": peer not found"; + return false; + } + + // Send request (non-blocking) + NOTIFY_REQUEST_CHUNK_HASH::request req; + req.chunk_index = chunk_index; + + bool sent = post_notify(*m_p2p, req, *peer_context); + if (!sent) { + logger(WARNING) << "[Chunk Validation] Failed to send chunk " << chunk_index << " hash request to peer " << peer_id; + return false; + } + + return true; +} + +bool ChunkValidationManager::store_chunk_hash_response(uint64_t peer_id, uint32_t chunk_index, const crypto::Hash& hash) +{ + uint64_t response_time = time(nullptr); + { + std::lock_guard validation_lock(m_pending_validations_mutex); + auto pending_it = m_pending_validations.find(chunk_index); + if (pending_it == m_pending_validations.end()) + { + return false; + } + + const std::vector& requested_peers = pending_it->second.requested_peers; + if (std::find(requested_peers.begin(), requested_peers.end(), peer_id) == requested_peers.end()) + { + return false; + } + + std::lock_guard lock(m_pending_chunk_hashes_mutex); + auto key = std::make_pair(peer_id, chunk_index); + ChunkHashResponse response; + response.hash = hash; + response.timestamp = response_time; + m_pending_chunk_hashes[key] = response; + } + + return true; +} + +void ChunkValidationManager::cleanup_chunk_responses(uint32_t chunk_index, const std::vector& peer_ids) +{ + std::lock_guard lock(m_pending_chunk_hashes_mutex); + for (uint64_t peer_id : peer_ids) + { + m_pending_chunk_hashes.erase(std::make_pair(peer_id, chunk_index)); + } +} + +bool ChunkValidationManager::is_chunk_being_validated(uint32_t chunk_index) const +{ + // Check if chunk is in pending validations (actual validation state) + // m_current_validating_chunk_index is cleared immediately after sending requests, + // but validation is still pending for up to 60 seconds, so we need to check m_pending_validations + std::lock_guard lock(m_pending_validations_mutex); + return (m_pending_validations.find(chunk_index) != m_pending_validations.end()); +} + +bool ChunkValidationManager::validate_unverified_chunks() +{ + // Check if we have unverified chunks first (fast check) + std::vector unverified_chunks = m_core.getCheckpointList().get_unverified_chunks(); + if (unverified_chunks.empty()) + { + return false; // No unverified chunks - nothing to validate + } + + // We need at least some peers to validate (but don't require full synchronization) + // Validation can happen during sync as long as we have peers + if (m_peersCount.load() == 0) + { + logger(DEBUGGING) << "[Chunk Validation] No peers available for validation"; + return false; + } + + // Rate limit: don't attempt validation more than once every P2P_CHECKPOINT_LIST_RE_REQUEST seconds (5 minutes) + // EXCEPTION: Allow immediate attempt if this is the first time we have unverified chunks + // (bypass rate limit on first attempt after chunks are created) + uint64_t time_now = time(nullptr); + bool bypass_rate_limit = false; + { + std::lock_guard lock(m_chunk_validation_mutex); + + // Check if this is the first validation attempt (m_last_chunk_validation_attempt == 0) + // or if enough time has passed + uint64_t time_since_last = time_now - m_last_chunk_validation_attempt; + + if (m_last_chunk_validation_attempt == 0) + { + // First validation attempt - allow it immediately + bypass_rate_limit = true; + logger(INFO) << "First chunk validation attempt - bypassing rate limit"; + } + else if (time_since_last < cn::P2P_CHECKPOINT_LIST_RE_REQUEST) + { + // Too soon since last attempt + logger(DEBUGGING) << "Chunk validation rate limited: " + << (cn::P2P_CHECKPOINT_LIST_RE_REQUEST - time_since_last) + << " seconds remaining"; + return false; + } + + m_last_chunk_validation_attempt = time_now; + } + + // Sort to ensure chronological order (oldest first) + std::sort(unverified_chunks.begin(), unverified_chunks.end()); + + logger(INFO) << "Attempting to validate " << unverified_chunks.size() + << " unverified chunk(s) via P2P consensus"; + + // Check if we're already validating a chunk + { + std::lock_guard lock(m_chunk_validation_mutex); + if (m_current_validating_chunk_index != UINT32_MAX) + { + // Already validating a chunk - wait for it to complete + return false; + } + } + + // Get current blockchain height to calculate peer uptime in blocks + uint32_t current_height; + crypto::Hash blockId; + m_core.get_blockchain_top(current_height, blockId); + uint32_t block_time = m_currency.difficultyTarget(); // Block time in seconds (120 for mainnet/testnet) + uint32_t min_uptime_blocks = m_core.getCheckpointList().get_min_peer_uptime_blocks(); + + // Find eligible peers (meet uptime requirement and support chunk-based checkpoints) + std::vector eligible_peers; + std::map peer_network_16; // peer_id -> /16 network + + // First, log all connected peers for debugging + uint32_t total_connected = 0; + m_p2p->for_each_connection([&](CryptoNoteConnectionContext& ctx, uint64_t peer_id) { + total_connected++; + }); + logger(INFO) << "[Chunk Validation] Checking " << total_connected << " connected peers for eligibility"; + + m_p2p->for_each_connection([&](CryptoNoteConnectionContext& ctx, uint64_t peer_id) { + // Only consider peers that support chunk-based checkpoints + if (ctx.version < cn::P2P_CHECKPOINT_LIST_VERSION) + { + logger(DEBUGGING) << "[Chunk Validation] Peer " << peer_id << " filtered: P2P version " << ctx.version << " < required version"; + return; // Skip old version peers + } + + // Accept peers in normal state (synchronized), idle state, OR synchronizing state + // We can validate chunks even during sync, as long as peers are connected + if (ctx.m_state != CryptoNoteConnectionContext::state_normal && + ctx.m_state != CryptoNoteConnectionContext::state_idle && + ctx.m_state != CryptoNoteConnectionContext::state_synchronizing) + { + logger(DEBUGGING) << "[Chunk Validation] Peer " << peer_id << " filtered: state " << get_protocol_state_string(ctx.m_state) << " (need normal, idle, or synchronizing)"; + return; // Skip peers that aren't in a usable state + } + + // Calculate peer uptime in blocks + // Uptime = (current_time - connection_start_time) / block_time + // This is an approximation - ideally we'd track peer's blockchain height when they first connected + time_t connection_duration = time_now - ctx.m_started; + if (connection_duration < 0) + { + logger(DEBUGGING) << "[Chunk Validation] Peer " << peer_id << " filtered: invalid connection time"; + return; // Invalid connection time + } + + uint32_t peer_uptime_blocks = static_cast(connection_duration / block_time); + + // Check if peer meets minimum uptime requirement + if (peer_uptime_blocks < min_uptime_blocks) + { + logger(DEBUGGING) << "[Chunk Validation] Peer " << peer_id << " filtered: uptime " << peer_uptime_blocks << " blocks < required"; + return; // Peer doesn't meet uptime requirement + } + + // Peer is eligible + logger(DEBUGGING) << "[Chunk Validation] Peer " << peer_id << " is ELIGIBLE (version: " << ctx.version << ", state: " + << get_protocol_state_string(ctx.m_state) << ", uptime: " << peer_uptime_blocks << " blocks)"; + eligible_peers.push_back(peer_id); + peer_network_16[peer_id] = CheckpointList::get_network_16(ctx.m_remote_ip); + }); + + if (eligible_peers.empty()) + { + logger(INFO) << "[Chunk Validation] No eligible peers found (need uptime > " << min_uptime_blocks << " blocks)"; + logger(INFO) << "Note: Only ACTIVE CONNECTIONS are considered, not peers in peerlist. " + << "Use 'print_cn' command to see active connections. " + << "To force connection to a peer, use --add-priority-node :"; + logger(INFO) << "Chunk validation will be retried when eligible peers become available"; + return false; + } + + logger(INFO) << "[Chunk Validation] Found " << eligible_peers.size() << " eligible peers (uptime > " << min_uptime_blocks << " blocks)"; + + // Calculate consensus requirements (M, K, n) based on available eligible peers + CheckpointList::ConsensusRequirements req = m_core.getCheckpointList().calculate_consensus_requirements(eligible_peers.size()); + + // STEP 1: Check if we have enough eligible peers to meet K requirement + // If not enough eligible peers, break and retry later when more peers become eligible + if (eligible_peers.size() < req.min_peers) + { + logger(INFO) << "[Chunk Validation] Not enough eligible peers: have " << eligible_peers.size() + << ", need K=" << req.min_peers << ". Will retry when more peers become available."; + return false; // Will retry via rate limiting mechanism (5 minute wait) + } + + + // Log all eligible peers for debugging + logger(DEBUGGING) << "[Chunk Validation] Eligible peers list:"; + m_p2p->for_each_connection([&](CryptoNoteConnectionContext& ctx, uint64_t peer_id) { + if (std::find(eligible_peers.begin(), eligible_peers.end(), peer_id) != eligible_peers.end()) { + logger(DEBUGGING) << " - Peer " << peer_id << " (version: " << ctx.version << ")"; + } + }); + + // Validate chunks in chronological order (oldest first) + // This ensures we catch divergences at the root cause + for (uint32_t chunk_index : unverified_chunks) + { + // Check if we should stop (node shutting down) + if (m_stop.load()) + { + break; + } + + // Mark this chunk as being validated + { + std::lock_guard lock(m_chunk_validation_mutex); + if (m_current_validating_chunk_index != UINT32_MAX) + { + // Another thread started validation + break; + } + m_current_validating_chunk_index = chunk_index; + } + + // Prefetched chunks are auto-confirmed when the local chain reaches the chunk + // boundary (add_chunk_from_block_ids verifies them). Running a separate peer + // consensus round on them is redundant and misleading — skip. + if (m_core.getCheckpointList().is_chunk_prefetched(chunk_index)) + { + logger(DEBUGGING) << "[Chunk Validation] Chunk " << chunk_index + << " is prefetched — skipping (auto-confirmed by local computation)"; + { + std::lock_guard lock(m_chunk_validation_mutex); + m_current_validating_chunk_index = UINT32_MAX; + } + continue; + } + + // Get local chunk hash + crypto::Hash local_chunk_hash = m_core.getCheckpointList().get_chunk_hash(chunk_index); + if (local_chunk_hash == NULL_HASH) + { + logger(WARNING) << "[Chunk Validation] Cannot validate chunk " << chunk_index << ": chunk hash not found in memory"; + { + std::lock_guard lock(m_chunk_validation_mutex); + m_current_validating_chunk_index = UINT32_MAX; + } + continue; + } + + logger(INFO) << "[Chunk Validation] Validating chunk " << chunk_index << " with " << eligible_peers.size() << " eligible peers"; + + // Check if this chunk is already being validated asynchronously + { + std::lock_guard lock(m_pending_validations_mutex); + if (m_pending_validations.find(chunk_index) != m_pending_validations.end()) + { + logger(DEBUGGING) << "[Chunk Validation] Chunk " << chunk_index << " is already being validated, skipping"; + { + std::lock_guard lock2(m_chunk_validation_mutex); + m_current_validating_chunk_index = UINT32_MAX; + } + continue; + } + } + + // Calculate consensus requirements (M, K, n) + CheckpointList::ConsensusRequirements req = m_core.getCheckpointList().calculate_consensus_requirements(eligible_peers.size()); + + // Sample K peers with network diversity using shared utility function + auto getPeerNetwork16 = [&peer_network_16](uint64_t peer_id) -> uint32_t { + auto it = peer_network_16.find(peer_id); + return (it != peer_network_16.end()) ? it->second : 0; + }; + + CheckpointList::PeerSamplingResult sampling_result = CheckpointList::sample_peers_with_diversity( + eligible_peers, + getPeerNetwork16, + req.min_peers); + + const std::vector& sampled_peers = sampling_result.sampled_peers; + const std::map& network_votes = sampling_result.network_votes; + + if (sampled_peers.empty()) + { + logger(WARNING) << "[Chunk Validation] Could not sample any peers for chunk " << chunk_index << " validation"; + { + std::lock_guard lock(m_chunk_validation_mutex); + m_current_validating_chunk_index = UINT32_MAX; + } + continue; + } + + // Verify we have at least n distinct networks (network diversity requirement) + uint32_t distinct_networks = static_cast(network_votes.size()); + + if (distinct_networks < req.min_diverse_networks) + { + logger(WARNING) << "[Chunk Validation] Could not achieve network diversity for chunk " << chunk_index + << ": have " << distinct_networks << " distinct networks, need " << req.min_diverse_networks; + { + std::lock_guard lock(m_chunk_validation_mutex); + m_current_validating_chunk_index = UINT32_MAX; + } + continue; + } + + logger(INFO) << "[Chunk Validation] Sampled " << sampled_peers.size() << " peers from " << distinct_networks + << " distinct networks (requirement: " << req.min_diverse_networks << " networks)"; + + // Log which peers were selected for debugging + logger(DEBUGGING) << "[Chunk Validation] Sampled peers for chunk " << chunk_index << ":"; + for (uint64_t peer_id : sampled_peers) { + logger(DEBUGGING) << " - Selected peer " << peer_id; + } + + // Send async requests to sampled peers + uint64_t request_time = time(nullptr); + std::vector sent_peers; + std::map sent_peer_networks; + for (uint64_t peer_id : sampled_peers) + { + if (send_chunk_hash_request_async(peer_id, chunk_index)) + { + sent_peers.push_back(peer_id); + sent_peer_networks[peer_id] = getPeerNetwork16(peer_id); + } + } + + if (sent_peers.size() < req.min_peers) + { + logger(WARNING) << "[Chunk Validation] Only sent " << sent_peers.size() + << " async request(s) for chunk " << chunk_index + << ", need K=" << req.min_peers << " for consensus"; + cleanup_chunk_responses(chunk_index, sent_peers); + { + std::lock_guard lock(m_chunk_validation_mutex); + m_current_validating_chunk_index = UINT32_MAX; + } + continue; + } + + // Store pending validation state + { + std::lock_guard lock(m_pending_validations_mutex); + PendingChunkValidation pending; + pending.chunk_index = chunk_index; + pending.request_timestamp = request_time; + pending.attempt_start_time = request_time; + pending.attempt_number = 1; + pending.requested_peers = sent_peers; + pending.peer_network_16 = sent_peer_networks; + pending.local_hash = local_chunk_hash; + pending.is_first_attempt = true; + pending.is_prefetch = false; + m_pending_validations[chunk_index] = pending; + } + + logger(INFO) << "[Chunk Validation] Sent chunk " << chunk_index << " hash requests to " << sent_peers.size() << " peers. Checking consensus in 60 seconds."; + + // Clear validation state (validation is now async - will be checked in check_pending_chunk_validations) + { + std::lock_guard lock(m_chunk_validation_mutex); + m_current_validating_chunk_index = UINT32_MAX; + } + + // Don't process next chunk until this one is validated (file must be sequential) + // The check_pending_chunk_validations() function will handle consensus checking + break; + } + + return true; +} + +void ChunkValidationManager::check_pending_chunk_validations() +{ + uint64_t time_now = time(nullptr); + const uint64_t CONSENSUS_WAIT_SECONDS = 60; // 60 seconds + const uint64_t RETRY_DELAY_SECONDS = 30; // 30 seconds delay before retry + + struct PendingAction + { + enum Type + { + ADD_VERIFIED_CHUNK, + ROLLBACK_DIVERGENT_CHUNK + }; + + Type type; + uint32_t chunk_index; + uint32_t rollback_height; + uint32_t last_valid_chunk; + }; + + std::vector actions; + + { + std::lock_guard lock(m_pending_validations_mutex); + + // Iterate through pending validations + for (auto it = m_pending_validations.begin(); it != m_pending_validations.end();) + { + uint32_t chunk_index = it->first; + PendingChunkValidation& pending = it->second; + + uint64_t elapsed = time_now - pending.request_timestamp; + + // Check if 3 minutes have passed since requests were sent + if (elapsed < CONSENSUS_WAIT_SECONDS) + { + // Not enough time has passed yet, skip this validation + ++it; + continue; + } + + // 3 minutes have passed - check for consensus + if (!pending.is_prefetch) + logger(INFO) << "[Chunk Validation] Checking consensus for chunk " << chunk_index + << " (elapsed: " << elapsed << " seconds, attempt " << pending.attempt_number << ")"; + + // Collect fresh responses from requested peers + std::vector votes; + { + std::lock_guard lock2(m_pending_chunk_hashes_mutex); + + for (uint64_t peer_id : pending.requested_peers) + { + auto response_it = m_pending_chunk_hashes.find(std::make_pair(peer_id, chunk_index)); + if (response_it != m_pending_chunk_hashes.end()) + { + CheckpointList::ConsensusVote vote; + vote.peer_id = peer_id; + vote.hash = response_it->second.hash; + vote.timestamp = response_it->second.timestamp; + votes.push_back(vote); + } + } + } + + // Calculate consensus requirements + CheckpointList::ConsensusRequirements req = m_core.getCheckpointList().calculate_consensus_requirements(pending.requested_peers.size()); + CheckpointList::ConsensusVoteResult vote_result = CheckpointList::evaluate_consensus_votes( + votes, + pending.local_hash, + pending.peer_network_16, + pending.request_timestamp, + req); + + if (!pending.is_prefetch) + logger(INFO) << "[Chunk Validation] Chunk " << chunk_index << " consensus check: received " << vote_result.responses_received + << " fresh response(s) from " << pending.requested_peers.size() << " requested peer(s). " + << "Agreements: " << vote_result.agreements << " (need M=" << req.min_agreements + << " from n=" << req.min_diverse_networks << " networks, got n=" + << vote_result.local_diverse_networks << "), " + << "NULL_HASH responses: " << vote_result.null_hash_responses; + + // --- Prefetch success path --- + // For prefetch rounds local_hash == NULL_HASH, so local_consensus is always false. + // Success is when M peers agree on a real (non-null) hash from enough networks, + // which evaluate_consensus_votes() reports as divergent_consensus. + if (pending.is_prefetch && vote_result.divergent_consensus + && vote_result.consensus_hash != NULL_HASH) + { + logger(INFO, BRIGHT_CYAN) + << "[Chunk Prefetch] Chunk " << chunk_index + << " consensus reached: " << vote_result.consensus_hash_votes + << " peers from " << vote_result.consensus_hash_diverse_networks + << " networks agree on hash " << vote_result.consensus_hash; + + crypto::Hash prefetch_hash = vote_result.consensus_hash; + std::vector peers_to_cleanup = pending.requested_peers; + it = m_pending_validations.erase(it); + cleanup_chunk_responses(chunk_index, peers_to_cleanup); + + // Store the prefetched hash (extends checkpoint zone immediately) + if (!m_core.getCheckpointList().add_prefetched_chunk(chunk_index, prefetch_hash)) + { + logger(WARNING) << "[Chunk Prefetch] add_prefetched_chunk(" << chunk_index << ") failed " + << "(chunk may already be locally computed — this is fine)"; + } + continue; + } + + // --- Normal validation success path --- + // Check if we have M agreements from enough networks (consensus reached) + if (vote_result.local_consensus) + { + logger(INFO, BRIGHT_GREEN) << "[Chunk Validation] Chunk " << chunk_index + << " validated via peer consensus (" << vote_result.agreements + << " agreements from " << vote_result.local_diverse_networks + << " networks, need M=" << req.min_agreements << ")"; + + std::vector peers_to_cleanup = pending.requested_peers; + it = m_pending_validations.erase(it); + cleanup_chunk_responses(chunk_index, peers_to_cleanup); + + PendingAction action; + action.type = PendingAction::ADD_VERIFIED_CHUNK; + action.chunk_index = chunk_index; + action.rollback_height = 0; + action.last_valid_chunk = 0; + actions.push_back(action); + continue; + } + + // If all responses are NULL_HASH or missing, peers don't have this chunk yet (not a divergence) + if (vote_result.responses_received == 0 || + (vote_result.responses_received == vote_result.null_hash_responses && !vote_result.divergent_consensus)) + { + const char* mode = pending.is_prefetch ? "prefetch" : "validation"; + logger(INFO) << "Chunk " << chunk_index + << " " << mode << ": No peers have this chunk in memory yet (all returned NULL_HASH or no response). " + << "This is normal if: (1) peers are using version 1 (don't support chunk checkpoints), " + << "or (2) peers haven't created this chunk yet. " + << "Will retry once peers have this chunk."; + + std::vector peers_to_cleanup = pending.requested_peers; + it = m_pending_validations.erase(it); + cleanup_chunk_responses(chunk_index, peers_to_cleanup); + continue; + } + + // Consensus not reached - check if we should retry + if (pending.is_first_attempt) + { + // First attempt failed - wait 1 more minute before retry + uint64_t total_elapsed = time_now - pending.attempt_start_time; + if (total_elapsed < (CONSENSUS_WAIT_SECONDS + RETRY_DELAY_SECONDS)) + { + // Still waiting for retry delay + ++it; + continue; + } + + // Retry delay passed - start second attempt + logger(INFO) << "Chunk " << chunk_index + << " consensus failed on first attempt. Starting second attempt..."; + + std::vector previous_peers = pending.requested_peers; + std::map previous_peer_networks = pending.peer_network_16; + cleanup_chunk_responses(chunk_index, previous_peers); + + uint64_t retry_time = time_now; + std::vector sent_peers; + std::map sent_peer_networks; + for (uint64_t peer_id : previous_peers) + { + if (send_chunk_hash_request_async(peer_id, chunk_index)) + { + sent_peers.push_back(peer_id); + auto network_it = previous_peer_networks.find(peer_id); + sent_peer_networks[peer_id] = (network_it != previous_peer_networks.end()) ? network_it->second : 0; + } + } + + if (sent_peers.size() >= req.min_peers) + { + // Update pending validation for second attempt + pending.request_timestamp = retry_time; + pending.attempt_number = 2; + pending.requested_peers = sent_peers; + pending.peer_network_16 = sent_peer_networks; + pending.is_first_attempt = false; + // is_prefetch is preserved from the first attempt (already set) + + logger(INFO) << "Sent second attempt async requests for chunk " << chunk_index + << " to " << sent_peers.size() << " peer(s). " + << "Will check for consensus after 60 seconds."; + } + else + { + logger(WARNING) << "Failed to send enough second attempt requests for chunk " << chunk_index + << ": sent " << sent_peers.size() << ", need K=" << req.min_peers; + it = m_pending_validations.erase(it); + cleanup_chunk_responses(chunk_index, sent_peers); + continue; + } + } + else + { + // Second attempt also failed - check if it's actual divergence or just no responses + if (vote_result.responses_received == 0 || + (vote_result.responses_received == vote_result.null_hash_responses && !vote_result.divergent_consensus)) + { + logger(INFO) << "Chunk " << chunk_index + << " validation: No peers have this chunk in memory yet after 2 attempts " + << "(all returned NULL_HASH or no response). " + << "This is normal if: (1) peers are using version 1 (don't support chunk checkpoints), " + << "or (2) peers haven't created this chunk yet. " + << "Will retry validation once peers create this chunk."; + + std::vector peers_to_cleanup = pending.requested_peers; + it = m_pending_validations.erase(it); + cleanup_chunk_responses(chunk_index, peers_to_cleanup); + continue; + } + + // Actual divergence: M peers agree on a different hash from enough networks + if (vote_result.divergent_consensus) + { + logger(ERROR, BRIGHT_RED) << "Chunk " << chunk_index + << " validation FAILED: peer consensus did not agree with local chunk hash " + << "after 2 attempts. M peers (" << vote_result.consensus_hash_votes + << ") from " << vote_result.consensus_hash_diverse_networks + << " networks agree on different hash: " << vote_result.consensus_hash + << " (local: " << pending.local_hash << "). " + << "This indicates blockchain divergence."; + + // Calculate rollback height (bottom of the chunk) + uint32_t chunk_size = m_core.getCheckpointList().get_chunk_size(); + uint32_t rollback_height = chunk_index * chunk_size; + + logger(ERROR, BRIGHT_RED) << "Rolling back blockchain to height " << rollback_height + << " (chunk " << chunk_index << " boundary) due to peer consensus divergence"; + + PendingAction action; + action.type = PendingAction::ROLLBACK_DIVERGENT_CHUNK; + action.chunk_index = chunk_index; + action.rollback_height = rollback_height; + action.last_valid_chunk = (chunk_index > 0) ? (chunk_index - 1) : 0; + actions.push_back(action); + + std::vector peers_to_cleanup = pending.requested_peers; + it = m_pending_validations.erase(it); + cleanup_chunk_responses(chunk_index, peers_to_cleanup); + continue; + } + + // Mixed responses (some NULL_HASH, some mismatches, but no diverse M agreement on a single different hash) + logger(WARNING) << "Chunk " << chunk_index + << " validation: Inconsistent results after 2 attempts. " + << "Some peers returned NULL_HASH, some returned different hashes, " + << "but no M peers from enough networks agreed on a single different hash. " + << "This may indicate network issues or peers still syncing. " + << "Will retry validation later."; + + std::vector peers_to_cleanup = pending.requested_peers; + it = m_pending_validations.erase(it); + cleanup_chunk_responses(chunk_index, peers_to_cleanup); + continue; + } + + ++it; + } + } + + for (const PendingAction& action : actions) + { + if (action.type == PendingAction::ADD_VERIFIED_CHUNK) + { + if (!m_core.getCheckpointList().add_verified_chunk_to_file(action.chunk_index)) + { + logger(ERROR) << "Failed to save validated chunk " << action.chunk_index << " to checkpoint.dat"; + } + } + else + { + if (!m_core.getCheckpointList().truncate_checkpoint_file(action.last_valid_chunk)) + { + logger(ERROR, BRIGHT_RED) << "Failed to truncate checkpoint.dat to chunk " << action.last_valid_chunk; + } + else + { + logger(INFO) << "Truncated checkpoint.dat to chunk " << action.last_valid_chunk; + } + + if (!m_core.rollback_chain_to(action.rollback_height)) + { + logger(ERROR, BRIGHT_RED) << "Failed to rollback blockchain to height " << action.rollback_height + << " - node may be in inconsistent state"; + } + else + { + logger(INFO, BRIGHT_GREEN) << "Successfully rolled back blockchain to height " << action.rollback_height; + } + } + } +} + +// --------------------------------------------------------------------------- +// Forward prefetch: fetch chunk hashes ahead of local chain tip +// --------------------------------------------------------------------------- + +bool ChunkValidationManager::prefetch_missing_chunks(uint32_t network_height) +{ + // Derive the actual network tip: peers advertise local_tip + 1, so subtract 1. + uint32_t net_tip = (network_height > 0) ? (network_height - 1) : 0; + + // Ask CheckpointList for the next chunk index we should prefetch. + uint32_t chunk_index = m_core.getCheckpointList().get_next_prefetchable_chunk_index(net_tip); + if (chunk_index == UINT32_MAX) + return false; // Nothing to prefetch + + // Don't duplicate an in-flight request. + if (is_chunk_being_validated(chunk_index)) + return false; + + // Already prefetched? + if (m_core.getCheckpointList().is_chunk_prefetched(chunk_index)) + return false; + + // Rate-limit: attempt at most once every 60 seconds. + uint64_t time_now = time(nullptr); + { + std::lock_guard lock(m_chunk_validation_mutex); + if (time_now - m_last_prefetch_attempt < 60) + return false; + m_last_prefetch_attempt = time_now; + } + + if (m_peersCount.load() == 0) + return false; + + // Collect eligible peers (same rules as validate_unverified_chunks). + uint32_t current_height; + crypto::Hash top_id; + m_core.get_blockchain_top(current_height, top_id); + uint32_t block_time = m_currency.difficultyTarget(); + uint32_t min_uptime_blocks = m_core.getCheckpointList().get_min_peer_uptime_blocks(); + + std::vector eligible_peers; + std::map peer_network_16; + + m_p2p->for_each_connection([&](CryptoNoteConnectionContext& ctx, uint64_t peer_id) { + if (ctx.version < cn::P2P_CHECKPOINT_LIST_VERSION) return; + if (ctx.m_state != CryptoNoteConnectionContext::state_normal + && ctx.m_state != CryptoNoteConnectionContext::state_idle + && ctx.m_state != CryptoNoteConnectionContext::state_synchronizing) return; + time_t duration = time_now - ctx.m_started; + if (duration < 0) return; + uint32_t uptime_blocks = static_cast(duration / block_time); + if (uptime_blocks < min_uptime_blocks) return; + eligible_peers.push_back(peer_id); + peer_network_16[peer_id] = CheckpointList::get_network_16(ctx.m_remote_ip); + }); + + if (eligible_peers.empty()) + { + logger(DEBUGGING) << "[Chunk Prefetch] No eligible peers for chunk " << chunk_index; + return false; + } + + CheckpointList::ConsensusRequirements req = + m_core.getCheckpointList().calculate_consensus_requirements(eligible_peers.size()); + + if (eligible_peers.size() < req.min_peers) + { + logger(DEBUGGING) << "[Chunk Prefetch] Not enough eligible peers: have " + << eligible_peers.size() << ", need K=" << req.min_peers; + return false; + } + + auto getPeerNetwork16 = [&peer_network_16](uint64_t pid) -> uint32_t { + auto it = peer_network_16.find(pid); + return (it != peer_network_16.end()) ? it->second : 0; + }; + + CheckpointList::PeerSamplingResult sampling = + CheckpointList::sample_peers_with_diversity(eligible_peers, getPeerNetwork16, req.min_peers); + + if (sampling.sampled_peers.empty()) + { + logger(WARNING) << "[Chunk Prefetch] Could not sample peers for chunk " << chunk_index; + return false; + } + + uint32_t distinct_networks = static_cast(sampling.network_votes.size()); + if (distinct_networks < req.min_diverse_networks) + { + logger(DEBUGGING) << "[Chunk Prefetch] Insufficient network diversity for chunk " << chunk_index + << ": have " << distinct_networks + << " distinct /16, need " << req.min_diverse_networks; + return false; + } + + // Send async requests. + std::vector sent_peers; + std::map sent_networks; + for (uint64_t pid : sampling.sampled_peers) + { + if (send_chunk_hash_request_async(pid, chunk_index)) + { + sent_peers.push_back(pid); + sent_networks[pid] = getPeerNetwork16(pid); + } + } + + if (sent_peers.size() < req.min_peers) + { + logger(WARNING) << "[Chunk Prefetch] Only sent " << sent_peers.size() + << " request(s) for chunk " << chunk_index + << ", need K=" << req.min_peers; + cleanup_chunk_responses(chunk_index, sent_peers); + return false; + } + + { + std::lock_guard lock(m_pending_validations_mutex); + PendingChunkValidation pending; + pending.chunk_index = chunk_index; + pending.request_timestamp = time_now; + pending.attempt_start_time = time_now; + pending.attempt_number = 1; + pending.requested_peers = sent_peers; + pending.peer_network_16 = sent_networks; + pending.local_hash = NULL_HASH; // no local hash yet + pending.is_first_attempt = true; + pending.is_prefetch = true; + m_pending_validations[chunk_index] = pending; + } + + logger(INFO, BRIGHT_CYAN) + << "[Chunk Prefetch] Requested chunk " << chunk_index + << " from " << sent_peers.size() << " peers across " << distinct_networks + << " networks. Will check consensus in 60 seconds."; + + return true; +} + +} // namespace cn + diff --git a/src/CryptoNoteProtocol/CryptoNoteProtocolHandlerChunk.h b/src/CryptoNoteProtocol/CryptoNoteProtocolHandlerChunk.h new file mode 100644 index 000000000..b1613dc3c --- /dev/null +++ b/src/CryptoNoteProtocol/CryptoNoteProtocolHandlerChunk.h @@ -0,0 +1,126 @@ +// Copyright (c) 2011-2017 The Cryptonote developers +// Copyright (c) 2017-2018 The Circle Foundation & Conceal Devs +// Copyright (c) 2018-2023 Conceal Network & Conceal Devs +// +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "CryptoNoteCore/CryptoNoteBasic.h" +#include "CryptoNoteCore/CheckpointList.h" +#include "CryptoNoteCore/Currency.h" +#include "CryptoNoteCore/ICore.h" +#include "P2p/NetNodeCommon.h" +#include + +namespace cn +{ + + /** + * ChunkValidationManager + * + * Handles asynchronous chunk validation via P2P consensus. + * Manages pending chunk hash requests, responses, and consensus checking. + */ + class ChunkValidationManager + { + public: + ChunkValidationManager(ICore& core, + IP2pEndpoint* p2p, + const Currency& currency, + logging::ILogger& log, + std::atomic& peersCount, + std::atomic& stop); + + // Update P2P endpoint (called when endpoint is set/updated) + void set_p2p_endpoint(IP2pEndpoint* p2p); + + // Validate unverified chunks in chronological order (oldest first) + // This ensures we catch divergences at the root cause and rollback to the correct point + // Only validates when peers meet uptime requirements + // @return true if validation was attempted (even if it failed) + bool validate_unverified_chunks(); + + // Check pending chunk validations for consensus (called periodically) + // Checks if we have M identical hashes within the time window (3 minutes) + // If consensus reached, processes it; if timeout, schedules retry + void check_pending_chunk_validations(); + + // Prefetch chunk hashes ahead of local chain to keep the checkpoint zone extended. + // Queries network peers for the next missing chunk hash; if M-of-K peers agree, + // stores it as a prefetched chunk so IBD can continue at fast speed. + // Called from on_idle() alongside validate_unverified_chunks(). + // @param network_height Estimated network tip (observed height - 1) + // @return true if a prefetch round was started + bool prefetch_missing_chunks(uint32_t network_height); + + // Send chunk hash request asynchronously (non-blocking) + // Returns true if request was sent successfully + bool send_chunk_hash_request_async(uint64_t peer_id, uint32_t chunk_index); + + // Store chunk hash response (called from message handler) + // Returns false for late, unsolicited, or non-requested peer responses. + bool store_chunk_hash_response(uint64_t peer_id, uint32_t chunk_index, const crypto::Hash& hash); + + // Check if chunk is currently being validated + bool is_chunk_being_validated(uint32_t chunk_index) const; + + private: + // Pending chunk hash responses: (peer_id, chunk_index) -> (hash, timestamp) + // Used for asynchronous consensus mechanism + struct ChunkHashResponse { + crypto::Hash hash; + uint64_t timestamp; // When response was received + }; + + // Pending chunk validation attempts: chunk_index -> validation state + struct PendingChunkValidation { + uint32_t chunk_index; + uint64_t request_timestamp; // When requests were sent + uint64_t attempt_start_time; // When this attempt started + uint32_t attempt_number; // 1 or 2 + std::vector requested_peers; // Peers we requested from + std::map peer_network_16; // peer_id -> /16 network + crypto::Hash local_hash; + bool is_first_attempt; + // is_prefetch == true: we asked the network for a chunk we don't have locally yet. + // local_hash is NULL_HASH; success means M peers agreed on a non-null hash that + // we store as a prefetched chunk to extend the checkpoint zone. + bool is_prefetch; + }; + + void cleanup_chunk_responses(uint32_t chunk_index, const std::vector& peer_ids); + + ICore& m_core; + IP2pEndpoint* m_p2p; + const Currency& m_currency; + logging::LoggerRef logger; + std::atomic& m_peersCount; + std::atomic& m_stop; + + // Pending chunk hash responses: (peer_id, chunk_index) -> (hash, timestamp) + mutable std::mutex m_pending_chunk_hashes_mutex; + std::map, ChunkHashResponse> m_pending_chunk_hashes; + + // Pending chunk validation attempts: chunk_index -> validation state + mutable std::mutex m_pending_validations_mutex; + std::map m_pending_validations; // chunk_index -> validation state + + // Chunk validation state: track which chunk we're currently validating + // This prevents duplicate validation attempts and ensures chronological order + mutable std::mutex m_chunk_validation_mutex; + uint32_t m_current_validating_chunk_index; // Currently validating chunk (or UINT32_MAX if none) + uint64_t m_last_chunk_validation_attempt; // Last time we attempted validation (to avoid spamming) + uint64_t m_last_prefetch_attempt; // Last time prefetch_missing_chunks() ran + }; + +} // namespace cn + diff --git a/src/Daemon/Daemon.cpp b/src/Daemon/Daemon.cpp index d805957ad..ef7387e2a 100644 --- a/src/Daemon/Daemon.cpp +++ b/src/Daemon/Daemon.cpp @@ -17,7 +17,7 @@ #include "Common/PathTools.h" #include "crypto/hash.h" #include "CryptoNoteConfig.h" -#include "CryptoNoteCore/Checkpoints.h" +#include "CryptoNoteCore/CheckpointList.h" #include "CryptoNoteCore/Core.h" #include "CryptoNoteCore/CoreConfig.h" #include "CryptoNoteCore/CryptoNoteTools.h" @@ -226,12 +226,6 @@ int main(int argc, char* argv[]) cn::Currency currency = currencyBuilder.currency(); cn::core ccore(currency, nullptr, logManager, vm["enable-blockchain-indexes"].as(), vm["enable-autosave"].as()); - cn::Checkpoints checkpoints(logManager); - checkpoints.set_testnet(coreConfig.testnet); - checkpoints.load_checkpoints(); - checkpoints.load_checkpoints_from_dns(); - ccore.set_checkpoints(std::move(checkpoints)); - NetNodeConfig netNodeConfig; netNodeConfig.init(vm); netNodeConfig.setTestnet(coreConfig.testnet); diff --git a/src/Daemon/DaemonCommandsHandler.cpp b/src/Daemon/DaemonCommandsHandler.cpp index ccb13ac84..61b966f7e 100644 --- a/src/Daemon/DaemonCommandsHandler.cpp +++ b/src/Daemon/DaemonCommandsHandler.cpp @@ -34,6 +34,7 @@ DaemonCommandsHandler::DaemonCommandsHandler(cn::core &core, cn::NodeServer &srv m_consoleHandler.setHandler("save", boost::bind(&DaemonCommandsHandler::save, this, boost::arg<1>()), "Save the Blockchain data safely"); m_consoleHandler.setHandler("print_pl", boost::bind(&DaemonCommandsHandler::print_pl, this, boost::arg<1>()), "Print peer list"); m_consoleHandler.setHandler("rollback_chain", boost::bind(&DaemonCommandsHandler::rollback_chain, this, boost::arg<1>()), "Rollback chain to specific height, rollback_chain "); + m_consoleHandler.setHandler("checkpoint_hash", boost::bind(&DaemonCommandsHandler::checkpoint_hash, this, boost::arg<1>()), "Calculate checkpoint hash up to "); m_consoleHandler.setHandler("print_cn", boost::bind(&DaemonCommandsHandler::print_cn, this, boost::arg<1>()), "Print connections"); m_consoleHandler.setHandler("print_bci", boost::bind(&DaemonCommandsHandler::print_bci, this, boost::arg<1>()), "Print blockchain current height"); m_consoleHandler.setHandler("print_bc", boost::bind(&DaemonCommandsHandler::print_bc, this, boost::arg<1>()), "Print blockchain info in a given blocks range, print_bc []"); @@ -309,6 +310,25 @@ bool DaemonCommandsHandler::rollback_chain(const std::vector &args) return true; } +bool DaemonCommandsHandler::checkpoint_hash(const std::vector &args) +{ + if (args.empty()) + { + logger(logging::ERROR) << "Usage: \"checkpoint_hash \""; + return true; + } + logger(logging::DEBUGGING) << "Attempting: checkpoint_hash"; + + const std::string &arg = args.front(); + uint32_t height = boost::lexical_cast(arg); + crypto::Hash hash = m_core.checkpoint_hash(height); + + logger(logging::INFO) << "Checkpoint hash: " << hash << " for " << height; + + logger(logging::DEBUGGING) << "Finished: checkpoint_hash"; + return true; +} + bool DaemonCommandsHandler::rollbackchainto(uint32_t height) { logger(logging::DEBUGGING) << "Attempting: rollbackchainto"; diff --git a/src/Daemon/DaemonCommandsHandler.h b/src/Daemon/DaemonCommandsHandler.h index f7bbc7088..7ad8c7dcc 100644 --- a/src/Daemon/DaemonCommandsHandler.h +++ b/src/Daemon/DaemonCommandsHandler.h @@ -52,7 +52,8 @@ class DaemonCommandsHandler bool show_hr(const std::vector& args); bool hide_hr(const std::vector& args); bool rollbackchainto(uint32_t height); - bool rollback_chain(const std::vector& args); + bool rollback_chain(const std::vector& args); + bool checkpoint_hash(const std::vector &args); bool print_cn(const std::vector& args); bool print_bc(const std::vector& args); bool print_bci(const std::vector& args); diff --git a/src/InProcessNode/InProcessNode.cpp b/src/InProcessNode/InProcessNode.cpp index e72be2730..f1de35903 100644 --- a/src/InProcessNode/InProcessNode.cpp +++ b/src/InProcessNode/InProcessNode.cpp @@ -63,13 +63,17 @@ void InProcessNode::init(const Callback& callback) { protocol.addObserver(this); core.addObserver(this); - work.reset(new boost::asio::io_service::work(ioService)); +#if BOOST_VERSION >= 109000 + work.reset(new InProcessNodeWork(ioService.get_executor())); +#else + work.reset(new InProcessNodeWork(ioService)); +#endif workerThread.reset(new std::thread(&InProcessNode::workerFunc, this)); state = INITIALIZED; } - ioService.post(std::bind(callback, ec)); + postIoService(std::bind(callback, ec)); } bool InProcessNode::shutdown() { @@ -89,10 +93,18 @@ bool InProcessNode::doShutdown() { work.reset(); ioService.stop(); workerThread->join(); - ioService.reset(); + resetIoService(); return true; } +void InProcessNode::resetIoService() { +#if BOOST_VERSION >= 109000 + ioService.restart(); +#else + ioService.reset(); +#endif +} + void InProcessNode::workerFunc() { ioService.run(); } @@ -107,7 +119,7 @@ void InProcessNode::getNewBlocks(std::vector&& knownBlockIds, std: return; } - ioService.post( + postIoService( std::bind(&InProcessNode::getNewBlocksAsync, this, std::move(knownBlockIds), @@ -181,7 +193,7 @@ void InProcessNode::getTransactionOutsGlobalIndices(const crypto::Hash& transact return; } - ioService.post( + postIoService( std::bind(&InProcessNode::getTransactionOutsGlobalIndicesAsync, this, std::cref(transactionHash), @@ -231,7 +243,7 @@ void InProcessNode::getRandomOutsByAmounts(std::vector&& amounts, uint return; } - ioService.post( + postIoService( std::bind(&InProcessNode::getRandomOutsByAmountsAsync, this, std::move(amounts), @@ -288,7 +300,7 @@ void InProcessNode::relayTransaction(const cn::Transaction& transaction, const C return; } - ioService.post( + postIoService( std::bind(&InProcessNode::relayTransactionAsync, this, transaction, @@ -457,7 +469,7 @@ void InProcessNode::queryBlocks(std::vector&& knownBlockIds, uint6 return; } - ioService.post( + postIoService( std::bind(&InProcessNode::queryBlocksLiteAsync, this, std::move(knownBlockIds), @@ -519,7 +531,7 @@ void InProcessNode::getPoolSymmetricDifference(std::vector&& known return; } - ioService.post([this, knownPoolTxIds, knownBlockId, &isBcActual, &newTxs, &deletedTxIds, callback] () mutable { + postIoService([this, knownPoolTxIds, knownBlockId, &isBcActual, &newTxs, &deletedTxIds, callback] () mutable { this->getPoolSymmetricDifferenceAsync(std::move(knownPoolTxIds), knownBlockId, isBcActual, newTxs, deletedTxIds, callback); }); } @@ -552,7 +564,7 @@ void InProcessNode::getMultisignatureOutputByGlobalIndex(uint64_t amount, uint32 return; } - ioService.post([this, amount, gindex, &out, callback]() mutable { + postIoService([this, amount, gindex, &out, callback]() mutable { this->getOutByMSigGIndexAsync(amount, gindex, out, callback); }); } @@ -577,7 +589,7 @@ void InProcessNode::getBlocks(const std::vector& blockHeights, std::ve return; } - ioService.post( + postIoService( std::bind( static_cast< void(InProcessNode::*)( @@ -661,7 +673,7 @@ void InProcessNode::getBlocks(const std::vector& blockHashes, std: return; } - ioService.post( + postIoService( std::bind( static_cast< void(InProcessNode::*)( @@ -724,7 +736,7 @@ void InProcessNode::getBlocks(uint64_t timestampBegin, uint64_t timestampEnd, ui return; } - ioService.post( + postIoService( std::bind( static_cast< void(InProcessNode::*)( @@ -800,7 +812,7 @@ void InProcessNode::getTransactions(const std::vector& transaction return; } - ioService.post( + postIoService( std::bind( static_cast< void(InProcessNode::*)( @@ -865,7 +877,7 @@ void InProcessNode::getPoolTransactions(uint64_t timestampBegin, uint64_t timest return; } - ioService.post( + postIoService( std::bind( &InProcessNode::getPoolTransactionsAsync, this, @@ -924,7 +936,7 @@ void InProcessNode::getTransactionsByPaymentId(const crypto::Hash& paymentId, st return; } - ioService.post( + postIoService( std::bind( &InProcessNode::getTransactionsByPaymentIdAsync, this, @@ -945,7 +957,7 @@ void InProcessNode::getTransaction(const crypto::Hash &transactionHash, cn::Tran return; } - ioService.post( + postIoService( std::bind( static_cast< void (InProcessNode::*)( @@ -1040,7 +1052,7 @@ void InProcessNode::isSynchronized(bool& syncStatus, const Callback& callback) { return; } - ioService.post( + postIoService( std::bind( &InProcessNode::isSynchronizedAsync, this, diff --git a/src/InProcessNode/InProcessNode.h b/src/InProcessNode/InProcessNode.h index a9b926875..e8abb6b6f 100644 --- a/src/InProcessNode/InProcessNode.h +++ b/src/InProcessNode/InProcessNode.h @@ -18,11 +18,20 @@ #include #include +#include namespace cn { class core; +#if BOOST_VERSION >= 109000 +using InProcessNodeIoContext = boost::asio::io_context; +using InProcessNodeWork = boost::asio::executor_work_guard; +#else +using InProcessNodeIoContext = boost::asio::io_service; +using InProcessNodeWork = boost::asio::io_service::work; +#endif + class InProcessNode : public INode, public cn::ICryptoNoteProtocolObserver, public cn::ICoreObserver { public: InProcessNode(cn::ICore& core, cn::ICryptoNoteProtocolQuery& protocol); @@ -124,6 +133,16 @@ class InProcessNode : public INode, public cn::ICryptoNoteProtocolObserver, publ std::error_code doGetTransaction(const crypto::Hash &transactionHash, cn::Transaction &transaction); void workerFunc(); bool doShutdown(); + void resetIoService(); + + template + void postIoService(Handler handler) { +#if BOOST_VERSION >= 109000 + boost::asio::post(ioService, handler); +#else + ioService.post(handler); +#endif + } enum State { NOT_INITIALIZED, @@ -135,9 +154,9 @@ class InProcessNode : public INode, public cn::ICryptoNoteProtocolObserver, publ cn::ICryptoNoteProtocolQuery& protocol; tools::ObserverManager observerManager; - boost::asio::io_service ioService; + InProcessNodeIoContext ioService; std::unique_ptr workerThread; - std::unique_ptr work; + std::unique_ptr work; BlockchainExplorerDataBuilder blockchainExplorerDataBuilder; diff --git a/src/Logging/FileLogger.cpp b/src/Logging/FileLogger.cpp index 8c8a39453..0727937d9 100644 --- a/src/Logging/FileLogger.cpp +++ b/src/Logging/FileLogger.cpp @@ -12,6 +12,14 @@ namespace logging { FileLogger::FileLogger(Level level) : StreamLogger(level) { } +FileLogger::~FileLogger() { + if (fileStream.is_open()) { + stream = nullptr; + fileStream.flush(); + fileStream.close(); + } +} + void FileLogger::init(const std::string& fileName) { fileStream.open(fileName, std::ios::app); StreamLogger::attachToStream(fileStream); diff --git a/src/Logging/FileLogger.h b/src/Logging/FileLogger.h index ec948a94f..6e25e6495 100644 --- a/src/Logging/FileLogger.h +++ b/src/Logging/FileLogger.h @@ -15,6 +15,7 @@ namespace logging { class FileLogger : public StreamLogger { public: FileLogger(Level level = DEBUGGING); + ~FileLogger(); void init(const std::string& filename); private: diff --git a/src/Logging/LoggerManager.cpp b/src/Logging/LoggerManager.cpp index 256a142e5..143243913 100644 --- a/src/Logging/LoggerManager.cpp +++ b/src/Logging/LoggerManager.cpp @@ -17,6 +17,12 @@ using common::JsonValue; LoggerManager::LoggerManager() { } +LoggerManager::~LoggerManager() { + std::unique_lock lock(reconfigureLock); + LoggerGroup::loggers.clear(); + loggers.clear(); +} + void LoggerManager::operator()(const std::string& category, Level level, boost::posix_time::ptime time, const std::string& body) { std::unique_lock lock(reconfigureLock); LoggerGroup::operator()(category, level, time, body); diff --git a/src/Logging/LoggerManager.h b/src/Logging/LoggerManager.h index c86556041..3d6122504 100644 --- a/src/Logging/LoggerManager.h +++ b/src/Logging/LoggerManager.h @@ -18,6 +18,7 @@ namespace logging { class LoggerManager : public LoggerGroup { public: LoggerManager(); + ~LoggerManager(); void configure(const common::JsonValue& val); virtual void operator()(const std::string& category, Level level, boost::posix_time::ptime time, const std::string& body) override; diff --git a/src/P2p/ConnectionContext.h b/src/P2p/ConnectionContext.h index 7ac24e29d..fa4f3d76a 100644 --- a/src/P2p/ConnectionContext.h +++ b/src/P2p/ConnectionContext.h @@ -27,6 +27,8 @@ struct CryptoNoteConnectionContext { uint32_t m_remote_port = 0; bool m_is_income = false; time_t m_started = 0; + bool m_checkpoints_not_in_sync = false; + bool m_active_checkpoint_req = false; enum state { state_befor_handshake = 0, //default state diff --git a/src/P2p/NetNode.cpp b/src/P2p/NetNode.cpp index 35ec0eb3b..4651f7963 100644 --- a/src/P2p/NetNode.cpp +++ b/src/P2p/NetNode.cpp @@ -64,13 +64,16 @@ size_t get_random_index_with_fixed_probability(size_t max_index) { void addPortMapping(const logging::LoggerRef& logger, uint32_t port) { // Add UPnP port mapping - logger(INFO) << "Attempting to add IGD port mapping."; - int result; - UPNPDev *deviceList = upnpDiscover(1000, nullptr, nullptr, 0, 0, 2, &result); + logger(INFO) << "Attempting to add IGD port mapping for port " << port << "."; + int discoverErr = 0; + UPNPDev *deviceList = upnpDiscover(1000, nullptr, nullptr, 0, 0, 2, &discoverErr); + logger(DEBUGGING) << "upnpDiscover returned devlist=" << (void*)deviceList + << " error=" << discoverErr; UPNPUrls urls; IGDdatas igdData; char lanAddress[64]; - result = UPNP_GetValidIGD(deviceList, &urls, &igdData, lanAddress, sizeof lanAddress); + int result = UPNP_GetValidIGD(deviceList, &urls, &igdData, lanAddress, sizeof lanAddress); + logger(DEBUGGING) << "UPNP_GetValidIGD result=" << result; freeUPNPDevlist(deviceList); if (result != 0) { if (result == 1) { @@ -80,19 +83,22 @@ void addPortMapping(const logging::LoggerRef& logger, uint32_t port) { portString.str().c_str(), lanAddress, "conceal", "TCP", nullptr, "0") != 0) { logger(ERROR) << "UPNP port mapping failed."; } else { - logger(INFO, BRIGHT_GREEN) << "Added IGD port mapping."; + logger(INFO, BRIGHT_GREEN) << "Added IGD port mapping for port " << port << "."; } } else if (result == 2) { logger(INFO) << "IGD was found but reported as not connected."; } else if (result == 3) { logger(INFO) << "UPnP device was found but not recognized as IGD."; } else { - logger(ERROR) << "UPNP_GetValidIGD returned an unknown result code."; + logger(ERROR) << "UPNP_GetValidIGD returned an unknown result code: " << result; } FreeUPNPUrls(&urls); } else { - logger(INFO) << "No IGD was found."; + logger(INFO) << "No IGD was found." + << "If incoming connections fail, verify router port " + << port + << " and firewall rules."; } } diff --git a/src/PaymentGateService/main.cpp b/src/PaymentGateService/main.cpp index 6307e6fc8..e0bd308c6 100644 --- a/src/PaymentGateService/main.cpp +++ b/src/PaymentGateService/main.cpp @@ -123,8 +123,10 @@ int daemonize() { int runDaemon() { #ifdef _WIN32 + // Mutable buffer: SERVICE_TABLE_ENTRY expects LPSTR (non-const); string literals are const. + static char serviceDisplayName[] = "Payment Gate"; SERVICE_TABLE_ENTRY serviceTable[] { - { "Payment Gate", serviceMain }, + { serviceDisplayName, serviceMain }, { NULL, NULL } }; diff --git a/src/Platform/Linux/System/Dispatcher.cpp b/src/Platform/Linux/System/Dispatcher.cpp index 76241e0bd..b4b23399b 100644 --- a/src/Platform/Linux/System/Dispatcher.cpp +++ b/src/Platform/Linux/System/Dispatcher.cpp @@ -133,6 +133,12 @@ Dispatcher::~Dispatcher() { delete ucontext; } + // Clean up mainContext.ucontext allocated in constructor + if (mainContext.ucontext != nullptr) { + delete static_cast(mainContext.ucontext); + mainContext.ucontext = nullptr; + } + while (!timers.empty()) { int result = ::close(timers.top()); assert(result == 0); @@ -439,6 +445,8 @@ void Dispatcher::contextProcedure(void* ucontext) { context.procedure(); } catch(std::exception&) { } + context.procedure = nullptr; + context.interruptProcedure = nullptr; if (context.group != nullptr) { if (context.groupPrev != nullptr) { diff --git a/src/Platform/OSX/System/Dispatcher.cpp b/src/Platform/OSX/System/Dispatcher.cpp index c3a1e6197..919983481 100644 --- a/src/Platform/OSX/System/Dispatcher.cpp +++ b/src/Platform/OSX/System/Dispatcher.cpp @@ -124,6 +124,12 @@ Dispatcher::~Dispatcher() { delete ucontext; } + // Clean up mainContext.uctx allocated in constructor + if (mainContext.uctx != nullptr) { + delete static_cast(mainContext.uctx); + mainContext.uctx = nullptr; + } + auto result = close(kqueue); assert(result != -1); result = pthread_mutex_destroy(reinterpret_cast(this->mutex)); @@ -406,6 +412,8 @@ void Dispatcher::contextProcedure(void* ucontext) { context.procedure(); } catch(std::exception&) { } + context.procedure = nullptr; + context.interruptProcedure = nullptr; if (context.group != nullptr) { if (context.groupPrev != nullptr) { diff --git a/src/Platform/Windows/System/Dispatcher.cpp b/src/Platform/Windows/System/Dispatcher.cpp index 9fdb2d4b2..daea8d994 100644 --- a/src/Platform/Windows/System/Dispatcher.cpp +++ b/src/Platform/Windows/System/Dispatcher.cpp @@ -390,6 +390,8 @@ void Dispatcher::contextProcedure() { context.procedure(); } catch (std::exception&) { } + context.procedure = nullptr; + context.interruptProcedure = nullptr; if (context.group != nullptr) { if (context.groupPrev != nullptr) { diff --git a/src/System/ContextGroup.cpp b/src/System/ContextGroup.cpp index 116a4dac0..0154ea663 100644 --- a/src/System/ContextGroup.cpp +++ b/src/System/ContextGroup.cpp @@ -84,6 +84,11 @@ void ContextGroup::wait() { dispatcher->dispatch(); assert(context == dispatcher->getCurrentContext()); } + + for (NativeContext* context = contextGroup.firstContext; context != nullptr; context = context->groupNext) { + context->procedure = nullptr; + context->interruptProcedure = nullptr; + } } } diff --git a/src/WalletLegacy/WalletLegacy.cpp b/src/WalletLegacy/WalletLegacy.cpp index fe621b99b..7be1f9fc3 100644 --- a/src/WalletLegacy/WalletLegacy.cpp +++ b/src/WalletLegacy/WalletLegacy.cpp @@ -1362,7 +1362,7 @@ bool WalletLegacy::getTxProof(crypto::Hash& txid, cn::AccountPublicAddress& addr try { crypto::generate_tx_proof(txid, R, address.viewPublicKey, rA, tx_key, sig); } - catch (std::runtime_error) { + catch (const std::runtime_error&) { return false; } diff --git a/tests/CoreTests/Chaingen.h b/tests/CoreTests/Chaingen.h index df514ca86..9103434c4 100644 --- a/tests/CoreTests/Chaingen.h +++ b/tests/CoreTests/Chaingen.h @@ -7,6 +7,7 @@ #include #include +#include #include "CryptoNoteCore/CoreConfig.h" #include "Common/CommandLine.h" @@ -399,9 +400,28 @@ inline bool do_replay_events(std::vector& events, t_test_class if (!r) return false; + // Each test gets its own unique temp directory so: + // 1. No production checkpoint.dat is loaded — chunk-based checkpoints + // covering small heights would bypass PoW for synthetic test blocks. + // 2. SwappedVector blockchain files from one test don't contaminate the next. + boost::filesystem::path testDir = boost::filesystem::temp_directory_path() / + boost::filesystem::unique_path("conceal_core_test_%%%%-%%%%"); + boost::filesystem::create_directories(testDir); + + // RAII guard: remove the temp dir when this scope exits (best effort). + struct TempDirGuard { + boost::filesystem::path path; + ~TempDirGuard() { + boost::system::error_code ec; + boost::filesystem::remove_all(path, ec); + } + } tempGuard{testDir}; + logging::ConsoleLogger logger; cn::CoreConfig coreConfig; coreConfig.init(vm); + coreConfig.configFolder = testDir.string(); + cn::MinerConfig emptyMinerConfig; cn::cryptonote_protocol_stub pr; //TODO: stub only for this kind of test, make real validation of relayed objects cn::core c(validator.currency(), &pr, logger); diff --git a/tests/UnitTests/Checkpoints.cpp b/tests/UnitTests/Checkpoints.cpp index 0188db3ff..c2ff5d427 100644 --- a/tests/UnitTests/Checkpoints.cpp +++ b/tests/UnitTests/Checkpoints.cpp @@ -5,140 +5,316 @@ #include "gtest/gtest.h" -#include "CryptoNoteCore/Checkpoints.h" +#include "CryptoNoteCore/CheckpointList.h" +#include "CryptoNoteCore/Blockchain.h" +#include "CryptoNoteCore/Currency.h" +#include "CryptoNoteCore/TransactionPool.h" +#include "CryptoNoteCore/ITransactionValidator.h" +#include "CryptoNoteCore/ITimeProvider.h" #include +#include +#include "crypto/hash.h" +#include +#include using namespace cn; -TEST(checkpoints_is_alternative_block_allowed, handles_empty_checkpoins) +// Stub implementation for testing +class TestTransactionValidator : public cn::ITransactionValidator { +public: + bool checkTransactionInputs(const cn::Transaction&, BlockInfo&) override { return true; } + bool checkTransactionInputs(const cn::Transaction&, BlockInfo&, BlockInfo&) override { return true; } + bool haveSpentKeyImages(const cn::Transaction&) override { return false; } + bool checkTransactionSize(size_t) override { return true; } +}; + +namespace { + +crypto::Hash testHash(const std::string& hex) { + crypto::Hash hash = NULL_HASH; + bool parsed = common::podFromHex(hex, hash); + EXPECT_TRUE(parsed); + return hash; +} + +CheckpointList::ConsensusRequirements testConsensusRequirements() { + CheckpointList::ConsensusRequirements req; + req.min_agreements = 2; + req.min_peers = 2; + req.min_diverse_networks = 2; + return req; +} + +CheckpointList::ConsensusVote testVote(uint64_t peer_id, const crypto::Hash& hash, uint64_t timestamp) { + CheckpointList::ConsensusVote vote; + vote.peer_id = peer_id; + vote.hash = hash; + vote.timestamp = timestamp; + return vote; +} + +} + +TEST(checkpoints_consensus_votes, ignores_stale_responses) +{ + crypto::Hash local_hash = testHash("1111111111111111111111111111111111111111111111111111111111111111"); + std::vector votes; + votes.push_back(testVote(1, local_hash, 10)); + votes.push_back(testVote(2, local_hash, 10)); + + std::map peer_networks; + peer_networks[1] = 0x0101; + peer_networks[2] = 0x0202; + + CheckpointList::ConsensusVoteResult result = CheckpointList::evaluate_consensus_votes( + votes, local_hash, peer_networks, 20, testConsensusRequirements()); + + ASSERT_EQ(0, result.responses_received); + ASSERT_EQ(0, result.agreements); + ASSERT_FALSE(result.local_consensus); +} + +TEST(checkpoints_consensus_votes, requires_diverse_networks_for_local_consensus) +{ + crypto::Hash local_hash = testHash("2222222222222222222222222222222222222222222222222222222222222222"); + std::vector votes; + votes.push_back(testVote(1, local_hash, 30)); + votes.push_back(testVote(2, local_hash, 30)); + + std::map peer_networks; + peer_networks[1] = 0x0101; + peer_networks[2] = 0x0101; + + CheckpointList::ConsensusVoteResult result = CheckpointList::evaluate_consensus_votes( + votes, local_hash, peer_networks, 20, testConsensusRequirements()); + + ASSERT_EQ(2, result.responses_received); + ASSERT_EQ(2, result.agreements); + ASSERT_EQ(1, result.local_diverse_networks); + ASSERT_FALSE(result.local_consensus); +} + +TEST(checkpoints_consensus_votes, accepts_diverse_local_consensus) +{ + crypto::Hash local_hash = testHash("3333333333333333333333333333333333333333333333333333333333333333"); + std::vector votes; + votes.push_back(testVote(1, local_hash, 30)); + votes.push_back(testVote(2, local_hash, 30)); + + std::map peer_networks; + peer_networks[1] = 0x0101; + peer_networks[2] = 0x0202; + + CheckpointList::ConsensusVoteResult result = CheckpointList::evaluate_consensus_votes( + votes, local_hash, peer_networks, 20, testConsensusRequirements()); + + ASSERT_EQ(2, result.responses_received); + ASSERT_EQ(2, result.agreements); + ASSERT_EQ(2, result.local_diverse_networks); + ASSERT_TRUE(result.local_consensus); +} + +TEST(checkpoints_consensus_votes, detects_diverse_divergent_consensus) +{ + crypto::Hash local_hash = testHash("4444444444444444444444444444444444444444444444444444444444444444"); + crypto::Hash peer_hash = testHash("5555555555555555555555555555555555555555555555555555555555555555"); + std::vector votes; + votes.push_back(testVote(1, peer_hash, 30)); + votes.push_back(testVote(2, peer_hash, 30)); + + std::map peer_networks; + peer_networks[1] = 0x0101; + peer_networks[2] = 0x0202; + + CheckpointList::ConsensusVoteResult result = CheckpointList::evaluate_consensus_votes( + votes, local_hash, peer_networks, 20, testConsensusRequirements()); + + ASSERT_FALSE(result.local_consensus); + ASSERT_TRUE(result.divergent_consensus); + ASSERT_EQ(peer_hash, result.consensus_hash); + ASSERT_EQ(2, result.consensus_hash_votes); + ASSERT_EQ(2, result.consensus_hash_diverse_networks); +} + +TEST(checkpoints_is_alternative_block_allowed, handles_empty_checkpoints) { logging::LoggerGroup logger; - Checkpoints cp(logger); + cn::Currency currency = cn::CurrencyBuilder(logger).testnet(true).currency(); + TestTransactionValidator validator; + cn::RealTimeProvider timeProvider; + cn::tx_memory_pool tx_pool(currency, validator, timeProvider, logger); + Blockchain blockchain(currency, tx_pool, logger, false, false); + + // Initialize with no checkpoints + blockchain.getCheckpointList().init_targets(true, ""); - ASSERT_FALSE(cp.is_alternative_block_allowed(0, 0)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(0, 0)); - ASSERT_TRUE(cp.is_alternative_block_allowed(1, 1)); - ASSERT_TRUE(cp.is_alternative_block_allowed(1, 9)); - ASSERT_TRUE(cp.is_alternative_block_allowed(9, 1)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 1)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 9)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(9, 1)); } TEST(checkpoints_is_alternative_block_allowed, handles_one_checkpoint) { logging::LoggerGroup logger; - Checkpoints cp(logger); - cp.add_checkpoint(5, "0000000000000000000000000000000000000000000000000000000000000000"); - - ASSERT_FALSE(cp.is_alternative_block_allowed(0, 0)); - - ASSERT_TRUE (cp.is_alternative_block_allowed(1, 1)); - ASSERT_TRUE (cp.is_alternative_block_allowed(1, 4)); - ASSERT_TRUE (cp.is_alternative_block_allowed(1, 5)); - ASSERT_TRUE (cp.is_alternative_block_allowed(1, 6)); - ASSERT_TRUE (cp.is_alternative_block_allowed(1, 9)); - - ASSERT_TRUE (cp.is_alternative_block_allowed(4, 1)); - ASSERT_TRUE (cp.is_alternative_block_allowed(4, 4)); - ASSERT_TRUE (cp.is_alternative_block_allowed(4, 5)); - ASSERT_TRUE (cp.is_alternative_block_allowed(4, 6)); - ASSERT_TRUE (cp.is_alternative_block_allowed(4, 9)); - - ASSERT_FALSE(cp.is_alternative_block_allowed(5, 1)); - ASSERT_FALSE(cp.is_alternative_block_allowed(5, 4)); - ASSERT_FALSE(cp.is_alternative_block_allowed(5, 5)); - ASSERT_TRUE (cp.is_alternative_block_allowed(5, 6)); - ASSERT_TRUE (cp.is_alternative_block_allowed(5, 9)); - - ASSERT_FALSE(cp.is_alternative_block_allowed(6, 1)); - ASSERT_FALSE(cp.is_alternative_block_allowed(6, 4)); - ASSERT_FALSE(cp.is_alternative_block_allowed(6, 5)); - ASSERT_TRUE (cp.is_alternative_block_allowed(6, 6)); - ASSERT_TRUE (cp.is_alternative_block_allowed(6, 9)); - - ASSERT_FALSE(cp.is_alternative_block_allowed(9, 1)); - ASSERT_FALSE(cp.is_alternative_block_allowed(9, 4)); - ASSERT_FALSE(cp.is_alternative_block_allowed(9, 5)); - ASSERT_TRUE (cp.is_alternative_block_allowed(9, 6)); - ASSERT_TRUE (cp.is_alternative_block_allowed(9, 9)); + cn::Currency currency = cn::CurrencyBuilder(logger).testnet(true).currency(); + TestTransactionValidator validator; + cn::RealTimeProvider timeProvider; + cn::tx_memory_pool tx_pool(currency, validator, timeProvider, logger); + Blockchain blockchain(currency, tx_pool, logger, false, false); + + blockchain.getCheckpointList().init_targets(true, ""); + // Reset to a clean slate so only our test targets exist + blockchain.getCheckpointList().clear_targets_for_test(); + + // Build a vector of 6 zero hashes (heights 0-5), mirroring old checkpoint.dat + std::vector checkpoint_list(6, NULL_HASH); + crypto::Hash list_hash = crypto::cn_fast_hash( + checkpoint_list.data(), checkpoint_list.size() * sizeof(crypto::Hash)); + + // Add checkpoint target at height 5 using the real list hash + blockchain.getCheckpointList().add_checkpoint_target_for_test(5, common::podToHex(list_hash)); + + // Load the checkpoint list into m_points (mirrors loading old checkpoint.dat) + ASSERT_TRUE(blockchain.getCheckpointList().set_checkpoint_list(std::move(checkpoint_list))); + + ASSERT_EQ(blockchain.getCheckpointList().get_greatest_target_height(), 5u); + + ASSERT_FALSE(blockchain.is_alternative_block_allowed(0, 0)); + + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 1)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 4)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 5)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 6)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 9)); + + ASSERT_TRUE(blockchain.is_alternative_block_allowed(4, 1)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(4, 4)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(4, 5)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(4, 6)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(4, 9)); + + // When blockchain_height >= checkpoint_height, blocks at or below checkpoint are not allowed + ASSERT_FALSE(blockchain.is_alternative_block_allowed(5, 1)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(5, 4)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(5, 5)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(5, 6)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(5, 9)); + + ASSERT_FALSE(blockchain.is_alternative_block_allowed(6, 1)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(6, 4)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(6, 5)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(6, 6)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(6, 9)); + + ASSERT_FALSE(blockchain.is_alternative_block_allowed(9, 1)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(9, 4)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(9, 5)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(9, 6)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(9, 9)); } TEST(checkpoints_is_alternative_block_allowed, handles_two_and_more_checkpoints) { logging::LoggerGroup logger; - Checkpoints cp(logger); - cp.add_checkpoint(5, "0000000000000000000000000000000000000000000000000000000000000000"); - cp.add_checkpoint(9, "0000000000000000000000000000000000000000000000000000000000000000"); - - ASSERT_FALSE(cp.is_alternative_block_allowed(0, 0)); - - ASSERT_TRUE (cp.is_alternative_block_allowed(1, 1)); - ASSERT_TRUE (cp.is_alternative_block_allowed(1, 4)); - ASSERT_TRUE (cp.is_alternative_block_allowed(1, 5)); - ASSERT_TRUE (cp.is_alternative_block_allowed(1, 6)); - ASSERT_TRUE (cp.is_alternative_block_allowed(1, 8)); - ASSERT_TRUE (cp.is_alternative_block_allowed(1, 9)); - ASSERT_TRUE (cp.is_alternative_block_allowed(1, 10)); - ASSERT_TRUE (cp.is_alternative_block_allowed(1, 11)); - - ASSERT_TRUE (cp.is_alternative_block_allowed(4, 1)); - ASSERT_TRUE (cp.is_alternative_block_allowed(4, 4)); - ASSERT_TRUE (cp.is_alternative_block_allowed(4, 5)); - ASSERT_TRUE (cp.is_alternative_block_allowed(4, 6)); - ASSERT_TRUE (cp.is_alternative_block_allowed(4, 8)); - ASSERT_TRUE (cp.is_alternative_block_allowed(4, 9)); - ASSERT_TRUE (cp.is_alternative_block_allowed(4, 10)); - ASSERT_TRUE (cp.is_alternative_block_allowed(4, 11)); - - ASSERT_FALSE(cp.is_alternative_block_allowed(5, 1)); - ASSERT_FALSE(cp.is_alternative_block_allowed(5, 4)); - ASSERT_FALSE(cp.is_alternative_block_allowed(5, 5)); - ASSERT_TRUE (cp.is_alternative_block_allowed(5, 6)); - ASSERT_TRUE (cp.is_alternative_block_allowed(5, 8)); - ASSERT_TRUE (cp.is_alternative_block_allowed(5, 9)); - ASSERT_TRUE (cp.is_alternative_block_allowed(5, 10)); - ASSERT_TRUE (cp.is_alternative_block_allowed(5, 11)); - - ASSERT_FALSE(cp.is_alternative_block_allowed(6, 1)); - ASSERT_FALSE(cp.is_alternative_block_allowed(6, 4)); - ASSERT_FALSE(cp.is_alternative_block_allowed(6, 5)); - ASSERT_TRUE (cp.is_alternative_block_allowed(6, 6)); - ASSERT_TRUE (cp.is_alternative_block_allowed(6, 8)); - ASSERT_TRUE (cp.is_alternative_block_allowed(6, 9)); - ASSERT_TRUE (cp.is_alternative_block_allowed(6, 10)); - ASSERT_TRUE (cp.is_alternative_block_allowed(6, 11)); - - ASSERT_FALSE(cp.is_alternative_block_allowed(8, 1)); - ASSERT_FALSE(cp.is_alternative_block_allowed(8, 4)); - ASSERT_FALSE(cp.is_alternative_block_allowed(8, 5)); - ASSERT_TRUE (cp.is_alternative_block_allowed(8, 6)); - ASSERT_TRUE (cp.is_alternative_block_allowed(8, 8)); - ASSERT_TRUE (cp.is_alternative_block_allowed(8, 9)); - ASSERT_TRUE (cp.is_alternative_block_allowed(8, 10)); - ASSERT_TRUE (cp.is_alternative_block_allowed(8, 11)); - - ASSERT_FALSE(cp.is_alternative_block_allowed(9, 1)); - ASSERT_FALSE(cp.is_alternative_block_allowed(9, 4)); - ASSERT_FALSE(cp.is_alternative_block_allowed(9, 5)); - ASSERT_FALSE(cp.is_alternative_block_allowed(9, 6)); - ASSERT_FALSE(cp.is_alternative_block_allowed(9, 8)); - ASSERT_FALSE(cp.is_alternative_block_allowed(9, 9)); - ASSERT_TRUE (cp.is_alternative_block_allowed(9, 10)); - ASSERT_TRUE (cp.is_alternative_block_allowed(9, 11)); - - ASSERT_FALSE(cp.is_alternative_block_allowed(10, 1)); - ASSERT_FALSE(cp.is_alternative_block_allowed(10, 4)); - ASSERT_FALSE(cp.is_alternative_block_allowed(10, 5)); - ASSERT_FALSE(cp.is_alternative_block_allowed(10, 6)); - ASSERT_FALSE(cp.is_alternative_block_allowed(10, 8)); - ASSERT_FALSE(cp.is_alternative_block_allowed(10, 9)); - ASSERT_TRUE (cp.is_alternative_block_allowed(10, 10)); - ASSERT_TRUE (cp.is_alternative_block_allowed(10, 11)); - - ASSERT_FALSE(cp.is_alternative_block_allowed(11, 1)); - ASSERT_FALSE(cp.is_alternative_block_allowed(11, 4)); - ASSERT_FALSE(cp.is_alternative_block_allowed(11, 5)); - ASSERT_FALSE(cp.is_alternative_block_allowed(11, 6)); - ASSERT_FALSE(cp.is_alternative_block_allowed(11, 8)); - ASSERT_FALSE(cp.is_alternative_block_allowed(11, 9)); - ASSERT_TRUE (cp.is_alternative_block_allowed(11, 10)); - ASSERT_TRUE (cp.is_alternative_block_allowed(11, 11)); + cn::Currency currency = cn::CurrencyBuilder(logger).testnet(true).currency(); + TestTransactionValidator validator; + cn::RealTimeProvider timeProvider; + cn::tx_memory_pool tx_pool(currency, validator, timeProvider, logger); + Blockchain blockchain(currency, tx_pool, logger, false, false); + + blockchain.getCheckpointList().init_targets(true, ""); + // Reset to a clean slate so only our test targets exist + blockchain.getCheckpointList().clear_targets_for_test(); + + // Build vectors of zero hashes mirroring old checkpoint.dat for each checkpoint + // Checkpoint at height 5: 6 hashes (heights 0-5) + std::vector list_5(6, NULL_HASH); + crypto::Hash hash_5 = crypto::cn_fast_hash(list_5.data(), list_5.size() * sizeof(crypto::Hash)); + blockchain.getCheckpointList().add_checkpoint_target_for_test(5, common::podToHex(hash_5)); + + // Checkpoint at height 9: 10 hashes (heights 0-9) + std::vector list_9(10, NULL_HASH); + crypto::Hash hash_9 = crypto::cn_fast_hash(list_9.data(), list_9.size() * sizeof(crypto::Hash)); + blockchain.getCheckpointList().add_checkpoint_target_for_test(9, common::podToHex(hash_9)); + + // Load the full checkpoint list into m_points (mirrors loading final checkpoint.dat) + ASSERT_TRUE(blockchain.getCheckpointList().set_checkpoint_list(std::move(list_9))); + + ASSERT_EQ(blockchain.getCheckpointList().get_greatest_target_height(), 9u); + + ASSERT_FALSE(blockchain.is_alternative_block_allowed(0, 0)); + + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 1)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 4)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 5)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 6)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 8)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 9)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 10)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(1, 11)); + + ASSERT_TRUE(blockchain.is_alternative_block_allowed(4, 1)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(4, 4)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(4, 5)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(4, 6)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(4, 8)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(4, 9)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(4, 10)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(4, 11)); + + ASSERT_FALSE(blockchain.is_alternative_block_allowed(5, 1)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(5, 4)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(5, 5)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(5, 6)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(5, 8)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(5, 9)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(5, 10)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(5, 11)); + + ASSERT_FALSE(blockchain.is_alternative_block_allowed(6, 1)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(6, 4)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(6, 5)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(6, 6)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(6, 8)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(6, 9)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(6, 10)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(6, 11)); + + ASSERT_FALSE(blockchain.is_alternative_block_allowed(8, 1)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(8, 4)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(8, 5)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(8, 6)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(8, 8)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(8, 9)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(8, 10)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(8, 11)); + + ASSERT_FALSE(blockchain.is_alternative_block_allowed(9, 1)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(9, 4)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(9, 5)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(9, 6)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(9, 8)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(9, 9)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(9, 10)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(9, 11)); + + ASSERT_FALSE(blockchain.is_alternative_block_allowed(10, 1)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(10, 4)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(10, 5)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(10, 6)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(10, 8)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(10, 9)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(10, 10)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(10, 11)); + + ASSERT_FALSE(blockchain.is_alternative_block_allowed(11, 1)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(11, 4)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(11, 5)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(11, 6)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(11, 8)); + ASSERT_FALSE(blockchain.is_alternative_block_allowed(11, 9)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(11, 10)); + ASSERT_TRUE(blockchain.is_alternative_block_allowed(11, 11)); } diff --git a/tests/UnitTests/ICoreStub.cpp b/tests/UnitTests/ICoreStub.cpp index de754050c..d495e7b1b 100644 --- a/tests/UnitTests/ICoreStub.cpp +++ b/tests/UnitTests/ICoreStub.cpp @@ -10,9 +10,11 @@ #include "CryptoNoteCore/IBlock.h" #include "CryptoNoteCore/VerificationContext.h" +#include ICoreStub::ICoreStub() : m_currency(cn::CurrencyBuilder(m_logger).currency()), + m_checkpoints(m_logger), topHeight(0), globalIndicesResult(false), randomOutsResult(false), @@ -22,6 +24,7 @@ ICoreStub::ICoreStub() : ICoreStub::ICoreStub(const cn::Block& genesisBlock) : m_currency(cn::CurrencyBuilder(m_logger).currency()), + m_checkpoints(m_logger), topHeight(0), globalIndicesResult(false), randomOutsResult(false), @@ -86,6 +89,10 @@ bool ICoreStub::handle_incoming_block(const cn::Block &b, cn::block_verification return false; } +cn::CheckpointList& ICoreStub::getCheckpointList() { + return m_checkpoints; +} + void ICoreStub::set_blockchain_top(uint32_t height, const crypto::Hash& top_id) { topHeight = height; topId = top_id; @@ -207,6 +214,26 @@ crypto::Hash ICoreStub::getBlockIdByHeight(uint32_t height) { return iter->second; } +std::vector ICoreStub::getBlockIds(uint32_t start_height, uint32_t end_height) { + std::vector result; + uint32_t height = start_height; + while (height <= end_height) { + auto iter = blockHashByHeightIndex.find(height); + if (iter == blockHashByHeightIndex.end()) { + break; + } + + result.push_back(iter->second); + if (height == std::numeric_limits::max()) { + break; + } + + ++height; + } + + return result; +} + bool ICoreStub::getBlockByHash(const crypto::Hash &h, cn::Block &blk) { auto iter = blocks.find(h); if (iter == blocks.end()) { @@ -375,6 +402,10 @@ bool ICoreStub::removeMessageQueue(cn::MessageQueue& mess return true; } +bool ICoreStub::rollback_chain_to(uint32_t height) { + return true; +} + void ICoreStub::setPoolChangesResult(bool result) { poolChangesResult = result; } diff --git a/tests/UnitTests/ICoreStub.h b/tests/UnitTests/ICoreStub.h index 7854d3f17..d89c6b6f3 100644 --- a/tests/UnitTests/ICoreStub.h +++ b/tests/UnitTests/ICoreStub.h @@ -9,6 +9,7 @@ #include #include "CryptoNoteCore/CryptoNoteBasic.h" +#include "CryptoNoteCore/CheckpointList.h" #include "CryptoNoteCore/ICore.h" #include "CryptoNoteCore/ICoreObserver.h" #include "CryptoNoteCore/Currency.h" @@ -59,10 +60,12 @@ class ICoreStub: public cn::ICore { bool handle_incoming_block(const cn::Block &b, cn::block_verification_context &bvc, bool control_miner, bool relay_block) override; virtual bool handle_get_objects(cn::NOTIFY_REQUEST_GET_OBJECTS::request& arg, cn::NOTIFY_RESPONSE_GET_OBJECTS::request& rsp) override { return false; } virtual void on_synchronized() override {} + virtual cn::CheckpointList& getCheckpointList() override; virtual bool getOutByMSigGIndex(uint64_t amount, uint64_t gindex, cn::MultisignatureOutput& out) override { return true; } virtual size_t addChain(const std::vector& chain) override; virtual crypto::Hash getBlockIdByHeight(uint32_t height) override; + virtual std::vector getBlockIds(uint32_t start_height, uint32_t end_height) override; virtual bool getBlockByHash(const crypto::Hash &h, cn::Block &blk) override; virtual bool getBlockHeight(const crypto::Hash& blockId, uint32_t& blockHeight) override; bool getTransaction(const crypto::Hash &id, cn::Transaction &tx, bool checkTxPool = false) override; @@ -90,6 +93,7 @@ class ICoreStub: public cn::ICore { virtual bool addMessageQueue(cn::MessageQueue& messageQueuePtr) override; virtual bool removeMessageQueue(cn::MessageQueue& messageQueuePtr) override; + virtual bool rollback_chain_to(uint32_t height) override; void set_blockchain_top(uint32_t height, const crypto::Hash& top_id); @@ -105,6 +109,7 @@ class ICoreStub: public cn::ICore { private: logging::ConsoleLogger m_logger; cn::Currency m_currency; + cn::CheckpointList m_checkpoints; uint32_t topHeight; crypto::Hash topId;