Merge pull request #4916 from pwojcikdev/ledger-verify-consistency

Verify ledger balance consistency on startup
This commit is contained in:
Piotr Wójcik 2025-06-23 11:53:50 +02:00 committed by GitHub
commit 71d8bae901
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 151 additions and 53 deletions

View file

@ -4623,7 +4623,7 @@ TEST (ledger, confirmation_height_not_updated)
ASSERT_EQ (nano::block_hash (0), confirmation_height_info.frontier); ASSERT_EQ (nano::block_hash (0), confirmation_height_info.frontier);
} }
TEST (ledger, zero_rep) TEST (ledger, zero_rep_weight)
{ {
nano::test::system system (1); nano::test::system system (1);
auto & node1 (*system.nodes[0]); auto & node1 (*system.nodes[0]);
@ -4640,7 +4640,7 @@ TEST (ledger, zero_rep)
auto transaction = node1.ledger.tx_begin_write (); auto transaction = node1.ledger.tx_begin_write ();
ASSERT_EQ (nano::block_status::progress, node1.ledger.process (transaction, block1)); 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 (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 () auto block2 = builder.state ()
.account (nano::dev::genesis_key.pub) .account (nano::dev::genesis_key.pub)
.previous (block1->hash ()) .previous (block1->hash ())

View file

@ -1373,7 +1373,7 @@ int main (int argc, char * const * argv)
auto node_flags = nano::inactive_node_flag_defaults (); auto node_flags = nano::inactive_node_flag_defaults ();
nano::update_flags (node_flags, vm); 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::inactive_node inactive_node_l (data_path, node_flags);
nano::node_rpc_config config; nano::node_rpc_config config;

View file

@ -369,7 +369,7 @@ void nano::block_processor::process_batch (nano::unique_lock<nano::mutex> & lock
} }
// We had rocksdb issues in the past, ensure that rep weights are always consistent // 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)) if (number_of_blocks_processed != 0 && timer.stop () > std::chrono::milliseconds (100))
{ {

View file

@ -345,7 +345,7 @@ std::deque<nano::block_hash> nano::bounded_backlog::perform_rollbacks (std::dequ
} }
// We had rocksdb issues in the past, ensure that rep weights are always consistent // 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; return processed;
} }

View file

@ -19,10 +19,7 @@ nano::node_flags const & nano::inactive_node_flag_defaults ()
static nano::node_flags node_flags; static nano::node_flags node_flags;
node_flags.inactive_node = true; node_flags.inactive_node = true;
node_flags.read_only = true; node_flags.read_only = true;
node_flags.generate_cache.reps = false; node_flags.generate_cache = nano::generate_cache_flags::all_disabled ();
node_flags.generate_cache.cemented_count = false;
node_flags.generate_cache.unchecked_count = false;
node_flags.generate_cache.account_count = false;
node_flags.disable_bootstrap_listener = true; node_flags.disable_bootstrap_listener = true;
node_flags.disable_tcp_realtime = true; node_flags.disable_tcp_realtime = true;
return node_flags; return node_flags;

View file

@ -22,7 +22,6 @@ add_library(
common.cpp common.cpp
fwd.hpp fwd.hpp
generate_cache_flags.hpp generate_cache_flags.hpp
generate_cache_flags.cpp
ledger.hpp ledger.hpp
ledger.cpp ledger.cpp
ledger_set_any.hpp ledger_set_any.hpp

View file

@ -1,9 +0,0 @@
#include <nano/secure/generate_cache_flags.hpp>
void nano::generate_cache_flags::enable_all ()
{
reps = true;
cemented_count = true;
unchecked_count = true;
account_count = true;
}

View file

@ -7,24 +7,29 @@ namespace nano
class generate_cache_flags class generate_cache_flags
{ {
public: public:
bool reps = true; bool reps{ true };
bool cemented_count = true; bool cemented_count{ true };
bool unchecked_count = true; bool unchecked_count{ true };
bool account_count = true; bool account_count{ true };
bool block_count = true; bool block_count{ true };
bool consistency_check{ true };
void enable_all ();
public: public:
static generate_cache_flags all_enabled ()
{
return {};
}
static generate_cache_flags all_disabled () static generate_cache_flags all_disabled ()
{ {
generate_cache_flags flags; return {
flags.reps = false; .reps = false,
flags.cemented_count = false; .cemented_count = false,
flags.unchecked_count = false; .unchecked_count = false,
flags.account_count = false; .account_count = false,
flags.block_count = false; .block_count = false,
return flags; .consistency_check = false,
};
} }
}; };
} }

View file

@ -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) 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 ( store.account.for_each_par (
[this] (store::read_transaction const &, auto i, auto n) { [this] (store::read_transaction const &, auto i, auto n) {
uint64_t block_count_l{ 0 }; 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.block_count += block_count_l;
this->cache.account_count += account_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) if (generate_cache_flags.reps)
{ {
logger.debug (nano::log::type::ledger, "Generating representative weights cache...");
store.rep_weight.for_each_par ( store.rep_weight.for_each_par (
[this] (store::read_transaction const &, auto i, auto n) { [this] (store::read_transaction const &, auto i, auto n) {
nano::rep_weights rep_weights_l{ this->store.rep_weight }; 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); 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 ( logger.debug (nano::log::type::ledger, "Verifying ledger balance consistency...");
[this] (store::read_transaction const &, auto i, auto n) {
uint64_t cemented_count_l (0); // Verify sum of all account and pending balances
nano::locked<nano::uint256_t> active_balance_s{ 0 };
nano::locked<nano::uint256_t> pending_balance_s{ 0 };
nano::locked<nano::uint256_t> 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) 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<nano::uint128_t>::max ());
release_assert (pending_balance <= std::numeric_limits<nano::uint128_t>::max ());
release_assert (burned_balance <= std::numeric_limits<nano::uint128_t>::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 ()); rep_weights.verify_consistency (static_cast<nano::uint128_t> (burned_balance));
cache.pruned_count = store.pruned.count (transaction);
} }
logger.info (nano::log::type::ledger, "Block count: {:>11}", cache.block_count.load ()); 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, "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, "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, "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<nano::uint128_t> (active_balance) }.format_balance (nano::nano_ratio, 0, true),
nano::uint128_union{ static_cast<nano::uint128_t> (pending_balance) }.format_balance (nano::nano_ratio, 0, true),
nano::uint128_union{ static_cast<nano::uint128_t> (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_committed () }.format_balance (nano::nano_ratio, 0, true),
nano::uint128_union{ rep_weights.get_weight_unused () }.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); 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::uint128_t result (0);
nano::account end (account_a.number () + 1); nano::account end (account_a.number () + 1);
@ -1267,7 +1363,6 @@ std::optional<nano::account> nano::ledger::linked_account (secure::transaction c
{ {
return any.block_account (transaction, block.source ()); return any.block_account (transaction, block.source ());
} }
return std::nullopt; return std::nullopt;
} }
@ -1602,7 +1697,6 @@ nano::epoch nano::ledger::version (nano::block const & block)
{ {
return block.sideband ().details.epoch; return block.sideband ().details.epoch;
} }
return nano::epoch::epoch_0; return nano::epoch::epoch_0;
} }

View file

@ -45,8 +45,8 @@ public:
/** Start read-only transaction */ /** Start read-only transaction */
secure::read_transaction tx_begin_read () const; secure::read_transaction tx_begin_read () const;
bool unconfirmed_exists (secure::transaction const &, nano::block_hash const &); bool unconfirmed_exists (secure::transaction const &, nano::block_hash const &) const;
nano::uint128_t account_receivable (secure::transaction const &, nano::account const &, bool = false); nano::uint128_t account_receivable (secure::transaction const &, nano::account const &, bool = false) const;
/** /**
* Returns the cached vote weight for the given representative. * Returns the cached vote weight for the given representative.
* If the weight is below the cache limit it returns 0. * If the weight is below the cache limit it returns 0.
@ -95,6 +95,8 @@ public:
using block_priority_result = std::pair<nano::amount, nano::priority_timestamp>; using block_priority_result = std::pair<nano::amount, nano::priority_timestamp>;
block_priority_result block_priority (secure::transaction const &, nano::block const &) const; 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; nano::container_info container_info () const;
public: public:

View file

@ -123,12 +123,17 @@ void nano::rep_weights::append_from (nano::rep_weights const & other)
weight_unused += other.weight_unused; 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 }; 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<nano::uint128_t>::max (), "total weight exceeds maximum value", to_string (weight_committed) + " + " + to_string (weight_unused)); release_assert (total_weight == std::numeric_limits<nano::uint128_t>::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<nano::uint128_t>::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; 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)); 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 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); auto it = rep_amounts.find (rep);
if (it != rep_amounts.end ()) if (it != rep_amounts.end ())
{ {

View file

@ -41,7 +41,7 @@ public:
nano::uint128_t get_weight_committed () const; nano::uint128_t get_weight_committed () const;
nano::uint128_t get_weight_unused () const; nano::uint128_t get_weight_unused () const;
void verify_consistency () const; void verify_consistency (nano::uint128_t burn_balance) const;
private: private:
nano::store::rep_weight & rep_weight_store; nano::store::rep_weight & rep_weight_store;