From c0297e22a9242dfdd6896fc6a1d7e32fe4ea9667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20W=C3=B3jcik?= <3044353+pwojcikdev@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:53:50 +0200 Subject: [PATCH] Merge pull request #4916 from pwojcikdev/ledger-verify-consistency Verify ledger balance consistency on startup # Conflicts: # nano/secure/generate_cache_flags.hpp --- nano/core_test/ledger.cpp | 4 +- nano/nano_node/entry.cpp | 2 +- nano/node/block_processor.cpp | 2 +- nano/node/bounded_backlog.cpp | 2 +- nano/node/inactive_node.cpp | 5 +- nano/secure/CMakeLists.txt | 1 - nano/secure/generate_cache_flags.cpp | 9 -- nano/secure/generate_cache_flags.hpp | 29 +++++-- nano/secure/ledger.cpp | 122 ++++++++++++++++++++++++--- nano/secure/ledger.hpp | 6 +- nano/secure/rep_weights.cpp | 16 +++- nano/secure/rep_weights.hpp | 2 +- 12 files changed, 155 insertions(+), 45 deletions(-) delete mode 100644 nano/secure/generate_cache_flags.cpp diff --git a/nano/core_test/ledger.cpp b/nano/core_test/ledger.cpp index 739aba905..f002aaf3c 100644 --- a/nano/core_test/ledger.cpp +++ b/nano/core_test/ledger.cpp @@ -4623,7 +4623,7 @@ TEST (ledger, confirmation_height_not_updated) ASSERT_EQ (nano::block_hash (0), confirmation_height_info.frontier); } -TEST (ledger, zero_rep) +TEST (ledger, zero_rep_weight) { nano::test::system system (1); auto & node1 (*system.nodes[0]); @@ -4640,7 +4640,7 @@ TEST (ledger, zero_rep) auto transaction = node1.ledger.tx_begin_write (); ASSERT_EQ (nano::block_status::progress, node1.ledger.process (transaction, block1)); ASSERT_EQ (0, node1.ledger.rep_weights.get (nano::dev::genesis_key.pub)); - ASSERT_EQ (nano::dev::constants.genesis_amount, node1.ledger.rep_weights.get (0)); + ASSERT_EQ (0, node1.ledger.rep_weights.get (0)); auto block2 = builder.state () .account (nano::dev::genesis_key.pub) .previous (block1->hash ()) diff --git a/nano/nano_node/entry.cpp b/nano/nano_node/entry.cpp index 3378009c2..e27e551cf 100644 --- a/nano/nano_node/entry.cpp +++ b/nano/nano_node/entry.cpp @@ -1373,7 +1373,7 @@ int main (int argc, char * const * argv) auto node_flags = nano::inactive_node_flag_defaults (); nano::update_flags (node_flags, vm); - node_flags.generate_cache.enable_all (); + node_flags.generate_cache = nano::generate_cache_flags::all_enabled (); nano::inactive_node inactive_node_l (data_path, node_flags); nano::node_rpc_config config; diff --git a/nano/node/block_processor.cpp b/nano/node/block_processor.cpp index bb51d79b0..f8065ac34 100644 --- a/nano/node/block_processor.cpp +++ b/nano/node/block_processor.cpp @@ -369,7 +369,7 @@ void nano::block_processor::process_batch (nano::unique_lock & lock } // We had rocksdb issues in the past, ensure that rep weights are always consistent - ledger.rep_weights.verify_consistency (); + ledger.verify_consistency (transaction); if (number_of_blocks_processed != 0 && timer.stop () > std::chrono::milliseconds (100)) { diff --git a/nano/node/bounded_backlog.cpp b/nano/node/bounded_backlog.cpp index f5a48b368..a51139549 100644 --- a/nano/node/bounded_backlog.cpp +++ b/nano/node/bounded_backlog.cpp @@ -345,7 +345,7 @@ std::deque nano::bounded_backlog::perform_rollbacks (std::dequ } // We had rocksdb issues in the past, ensure that rep weights are always consistent - ledger.rep_weights.verify_consistency (); + ledger.verify_consistency (transaction); return processed; } diff --git a/nano/node/inactive_node.cpp b/nano/node/inactive_node.cpp index 69696609d..55225594c 100644 --- a/nano/node/inactive_node.cpp +++ b/nano/node/inactive_node.cpp @@ -19,10 +19,7 @@ nano::node_flags const & nano::inactive_node_flag_defaults () static nano::node_flags node_flags; node_flags.inactive_node = true; node_flags.read_only = true; - node_flags.generate_cache.reps = false; - node_flags.generate_cache.cemented_count = false; - node_flags.generate_cache.unchecked_count = false; - node_flags.generate_cache.account_count = false; + node_flags.generate_cache = nano::generate_cache_flags::all_disabled (); node_flags.disable_bootstrap_listener = true; node_flags.disable_tcp_realtime = true; return node_flags; diff --git a/nano/secure/CMakeLists.txt b/nano/secure/CMakeLists.txt index 795d7002b..bbb864c17 100644 --- a/nano/secure/CMakeLists.txt +++ b/nano/secure/CMakeLists.txt @@ -22,7 +22,6 @@ add_library( common.cpp fwd.hpp generate_cache_flags.hpp - generate_cache_flags.cpp ledger.hpp ledger.cpp ledger_set_any.hpp diff --git a/nano/secure/generate_cache_flags.cpp b/nano/secure/generate_cache_flags.cpp deleted file mode 100644 index 912932cee..000000000 --- a/nano/secure/generate_cache_flags.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include - -void nano::generate_cache_flags::enable_all () -{ - reps = true; - cemented_count = true; - unchecked_count = true; - account_count = true; -} diff --git a/nano/secure/generate_cache_flags.hpp b/nano/secure/generate_cache_flags.hpp index 29445e53d..94a871b1a 100644 --- a/nano/secure/generate_cache_flags.hpp +++ b/nano/secure/generate_cache_flags.hpp @@ -7,12 +7,29 @@ namespace nano class generate_cache_flags { public: - bool reps = true; - bool cemented_count = true; - bool unchecked_count = true; - bool account_count = true; - bool block_count = true; + bool reps{ true }; + bool cemented_count{ true }; + bool unchecked_count{ true }; + bool account_count{ true }; + bool block_count{ true }; + bool consistency_check{ true }; - void enable_all (); +public: + static generate_cache_flags all_enabled () + { + return {}; + } + + static generate_cache_flags all_disabled () + { + return { + .reps = false, + .cemented_count = false, + .unchecked_count = false, + .account_count = false, + .block_count = false, + .consistency_check = false, + }; + } }; } diff --git a/nano/secure/ledger.cpp b/nano/secure/ledger.cpp index 776e9e224..73069d4e3 100644 --- a/nano/secure/ledger.cpp +++ b/nano/secure/ledger.cpp @@ -790,6 +790,8 @@ void nano::ledger::initialize (nano::generate_cache_flags const & generate_cache if (generate_cache_flags.account_count || generate_cache_flags.block_count) { + logger.debug (nano::log::type::ledger, "Generating block count cache..."); + store.account.for_each_par ( [this] (store::read_transaction const &, auto i, auto n) { uint64_t block_count_l{ 0 }; @@ -803,10 +805,40 @@ void nano::ledger::initialize (nano::generate_cache_flags const & generate_cache this->cache.block_count += block_count_l; this->cache.account_count += account_count_l; }); + + logger.debug (nano::log::type::ledger, "Block count cache generated"); + } + + if (generate_cache_flags.cemented_count) + { + logger.debug (nano::log::type::ledger, "Generating cemented count cache..."); + + store.confirmation_height.for_each_par ( + [this] (store::read_transaction const &, auto i, auto n) { + uint64_t cemented_count_l (0); + for (; i != n; ++i) + { + cemented_count_l += i->second.height; + } + this->cache.cemented_count += cemented_count_l; + }); + + logger.debug (nano::log::type::ledger, "Cemented count cache generated"); + } + + { + logger.debug (nano::log::type::ledger, "Generating pruned count cache..."); + + auto transaction = store.tx_begin_read (); + cache.pruned_count = store.pruned.count (transaction); + + logger.debug (nano::log::type::ledger, "Pruned count cache generated"); } if (generate_cache_flags.reps) { + logger.debug (nano::log::type::ledger, "Generating representative weights cache..."); + store.rep_weight.for_each_par ( [this] (store::read_transaction const &, auto i, auto n) { nano::rep_weights rep_weights_l{ this->store.rep_weight }; @@ -827,25 +859,80 @@ void nano::ledger::initialize (nano::generate_cache_flags const & generate_cache this->rep_weights.append_from (rep_weights_l); }); - rep_weights.verify_consistency (); + logger.debug (nano::log::type::ledger, "Representative weights cache generated"); } - if (generate_cache_flags.cemented_count) + // Use larger precision types to detect potential overflow issues + nano::uint256_t active_balance, pending_balance, burned_balance; + + if (generate_cache_flags.consistency_check) { - store.confirmation_height.for_each_par ( - [this] (store::read_transaction const &, auto i, auto n) { - uint64_t cemented_count_l (0); + logger.debug (nano::log::type::ledger, "Verifying ledger balance consistency..."); + + // Verify sum of all account and pending balances + nano::locked active_balance_s{ 0 }; + nano::locked pending_balance_s{ 0 }; + nano::locked burned_balance_s{ 0 }; + + store.account.for_each_par ( + [&] (store::read_transaction const &, auto i, auto n) { + nano::uint256_t balance_l{ 0 }; + nano::uint256_t burned_l{ 0 }; for (; i != n; ++i) { - cemented_count_l += i->second.height; + nano::account_info const & info = i->second; + if (i->first == constants.burn_account) + { + burned_l += info.balance.number (); + } + else + { + balance_l += info.balance.number (); + } } - this->cache.cemented_count += cemented_count_l; + (*active_balance_s.lock ()) += balance_l; + release_assert (burned_l == 0); // The burn account should not have any active balance }); + + store.pending.for_each_par ( + [&] (store::read_transaction const &, auto i, auto n) { + nano::uint256_t balance_l{ 0 }; + nano::uint256_t burned_l{ 0 }; + for (; i != n; ++i) + { + nano::pending_key const & key = i->first; + nano::pending_info const & info = i->second; + if (key.account == constants.burn_account) + { + burned_l += info.amount.number (); + } + else + { + balance_l += info.amount.number (); + } + } + (*pending_balance_s.lock ()) += balance_l; + (*burned_balance_s.lock ()) += burned_l; + }); + + active_balance = *active_balance_s.lock (); + pending_balance = *pending_balance_s.lock (); + burned_balance = *burned_balance_s.lock (); + + release_assert (active_balance <= std::numeric_limits::max ()); + release_assert (pending_balance <= std::numeric_limits::max ()); + release_assert (burned_balance <= std::numeric_limits::max ()); + + release_assert (active_balance + pending_balance + burned_balance == constants.genesis_amount, "ledger corruption detected: account and pending balances do not match genesis amount", to_string (active_balance) + " + " + to_string (pending_balance) + " + " + to_string (burned_balance) + " != " + to_string (constants.genesis_amount)); + release_assert (active_balance == rep_weights.get_weight_committed (), "ledger corruption detected: active balance does not match committed representative weights", to_string (active_balance) + " != " + to_string (rep_weights.get_weight_committed ())); + release_assert (pending_balance + burned_balance == rep_weights.get_weight_unused (), "ledger corruption detected: pending balance does not match unused representative weights", to_string (pending_balance) + " != " + to_string (rep_weights.get_weight_unused ())); + + logger.debug (nano::log::type::ledger, "Ledger balance consistency verified"); } + if (generate_cache_flags.reps && generate_cache_flags.consistency_check) { - auto transaction (store.tx_begin_read ()); - cache.pruned_count = store.pruned.count (transaction); + rep_weights.verify_consistency (static_cast (burned_balance)); } logger.info (nano::log::type::ledger, "Block count: {:>11}", cache.block_count.load ()); @@ -853,17 +940,26 @@ void nano::ledger::initialize (nano::generate_cache_flags const & generate_cache logger.info (nano::log::type::ledger, "Account count: {:>11}", cache.account_count.load ()); logger.info (nano::log::type::ledger, "Pruned count: {:>11}", cache.pruned_count.load ()); logger.info (nano::log::type::ledger, "Representative count: {:>5}", rep_weights.size ()); - logger.info (nano::log::type::ledger, "Weight commited: {} | unused: {}", + logger.info (nano::log::type::ledger, "Active balance: {} | pending: {} | burned: {}", + nano::uint128_union{ static_cast (active_balance) }.format_balance (nano::nano_ratio, 0, true), + nano::uint128_union{ static_cast (pending_balance) }.format_balance (nano::nano_ratio, 0, true), + nano::uint128_union{ static_cast (burned_balance) }.format_balance (nano::nano_ratio, 0, true)); + logger.info (nano::log::type::ledger, "Weight committed: {} | unused: {}", nano::uint128_union{ rep_weights.get_weight_committed () }.format_balance (nano::nano_ratio, 0, true), nano::uint128_union{ rep_weights.get_weight_unused () }.format_balance (nano::nano_ratio, 0, true)); } -bool nano::ledger::unconfirmed_exists (secure::transaction const & transaction, nano::block_hash const & hash) +void nano::ledger::verify_consistency (secure::transaction const & transaction) const +{ + rep_weights.verify_consistency (0); // It's impractical to recompute burned weight, so we skip it here +} + +bool nano::ledger::unconfirmed_exists (secure::transaction const & transaction, nano::block_hash const & hash) const { return any.block_exists (transaction, hash) && !confirmed.block_exists (transaction, hash); } -nano::uint128_t nano::ledger::account_receivable (secure::transaction const & transaction_a, nano::account const & account_a, bool only_confirmed_a) +nano::uint128_t nano::ledger::account_receivable (secure::transaction const & transaction_a, nano::account const & account_a, bool only_confirmed_a) const { nano::uint128_t result (0); nano::account end (account_a.number () + 1); @@ -1267,7 +1363,6 @@ std::optional nano::ledger::linked_account (secure::transaction c { return any.block_account (transaction, block.source ()); } - return std::nullopt; } @@ -1602,7 +1697,6 @@ nano::epoch nano::ledger::version (nano::block const & block) { return block.sideband ().details.epoch; } - return nano::epoch::epoch_0; } diff --git a/nano/secure/ledger.hpp b/nano/secure/ledger.hpp index c48b1d67f..802d13de1 100644 --- a/nano/secure/ledger.hpp +++ b/nano/secure/ledger.hpp @@ -45,8 +45,8 @@ public: /** Start read-only transaction */ secure::read_transaction tx_begin_read () const; - bool unconfirmed_exists (secure::transaction const &, nano::block_hash const &); - nano::uint128_t account_receivable (secure::transaction const &, nano::account const &, bool = false); + bool unconfirmed_exists (secure::transaction const &, nano::block_hash const &) const; + nano::uint128_t account_receivable (secure::transaction const &, nano::account const &, bool = false) const; /** * Returns the cached vote weight for the given representative. * If the weight is below the cache limit it returns 0. @@ -95,6 +95,8 @@ public: using block_priority_result = std::pair; block_priority_result block_priority (secure::transaction const &, nano::block const &) const; + void verify_consistency (secure::transaction const &) const; + nano::container_info container_info () const; public: diff --git a/nano/secure/rep_weights.cpp b/nano/secure/rep_weights.cpp index 3e3d72a4f..9afc3cdeb 100644 --- a/nano/secure/rep_weights.cpp +++ b/nano/secure/rep_weights.cpp @@ -123,12 +123,17 @@ void nano::rep_weights::append_from (nano::rep_weights const & other) weight_unused += other.weight_unused; } -void nano::rep_weights::verify_consistency () const +void nano::rep_weights::verify_consistency (nano::uint128_t const burn_balance) const { std::shared_lock guard{ mutex }; - auto total_weight = weight_committed + weight_unused; + + auto const total_weight = weight_committed + weight_unused; release_assert (total_weight == std::numeric_limits::max (), "total weight exceeds maximum value", to_string (weight_committed) + " + " + to_string (weight_unused)); - auto cached_weight = std::accumulate (rep_amounts.begin (), rep_amounts.end (), nano::uint256_t{ 0 }, [] (nano::uint256_t sum, const auto & entry) { + + auto const expected_total = std::numeric_limits::max () - burn_balance; + release_assert (weight_committed <= expected_total, "total weight does not match expected value accounting for burn", to_string (weight_committed) + " + " + to_string (weight_unused) + " != " + to_string (expected_total) + " (burn: " + to_string (burn_balance) + ")"); + + auto const cached_weight = std::accumulate (rep_amounts.begin (), rep_amounts.end (), nano::uint256_t{ 0 }, [] (nano::uint256_t sum, const auto & entry) { return sum + entry.second; }); release_assert (cached_weight <= weight_committed, "total cached weight must match the sum of all committed weights", to_string (cached_weight) + " <= " + to_string (weight_committed)); @@ -178,6 +183,11 @@ void nano::rep_weights::put_store (store::write_transaction const & txn, nano::a nano::uint128_t nano::rep_weights::get_impl (nano::account const & rep) const { + if (rep.is_zero ()) + { + return 0; // Zero account always has zero weight + } + auto it = rep_amounts.find (rep); if (it != rep_amounts.end ()) { diff --git a/nano/secure/rep_weights.hpp b/nano/secure/rep_weights.hpp index 5999b7d58..9ad28a671 100644 --- a/nano/secure/rep_weights.hpp +++ b/nano/secure/rep_weights.hpp @@ -41,7 +41,7 @@ public: nano::uint128_t get_weight_committed () const; nano::uint128_t get_weight_unused () const; - void verify_consistency () const; + void verify_consistency (nano::uint128_t burn_balance) const; private: nano::store::rep_weight & rep_weight_store;