From 69091c0affe4cd1d38b536b035b673c6fc5264b2 Mon Sep 17 00:00:00 2001 From: Colin LeMahieu Date: Tue, 14 Nov 2023 09:44:45 +0000 Subject: [PATCH] Block checker Adding block_checker_context class which validates blocks outside the ledger class. --- nano/secure/CMakeLists.txt | 2 + nano/secure/block_check_context.cpp | 555 ++++++++++++++++++++++++++++ nano/secure/block_check_context.hpp | 117 ++++++ 3 files changed, 674 insertions(+) create mode 100644 nano/secure/block_check_context.cpp create mode 100644 nano/secure/block_check_context.hpp diff --git a/nano/secure/CMakeLists.txt b/nano/secure/CMakeLists.txt index 45c51a62f5..690bf323b6 100644 --- a/nano/secure/CMakeLists.txt +++ b/nano/secure/CMakeLists.txt @@ -44,6 +44,8 @@ add_library( account_iterator.cpp account_iterator.hpp account_iterator_impl.hpp + block_check_context.cpp + block_check_context.hpp block_delta.hpp common.hpp common.cpp diff --git a/nano/secure/block_check_context.cpp b/nano/secure/block_check_context.cpp new file mode 100644 index 0000000000..6dd1df7204 --- /dev/null +++ b/nano/secure/block_check_context.cpp @@ -0,0 +1,555 @@ +#include +#include +#include +#include +#include +#include +#include + +nano::block_check_context::block_check_context (nano::ledger & ledger, std::shared_ptr block) : + block_m{ block }, + ledger{ ledger } +{ +} + +auto nano::block_check_context::op () const -> block_op +{ + debug_assert (state.has_value ()); + switch (block_m->type ()) + { + case nano::block_type::state: + if (block_m->balance_field ().value () < state->balance) + { + return block_op::send; + } + if (previous != nullptr && block_m->link_field ().value ().is_zero ()) + { + return block_op::noop; + } + if (ledger.constants.epochs.is_epoch_link (block_m->link_field ().value ())) + { + return block_op::epoch; + } + return block_op::receive; + case nano::block_type::send: + return block_op::send; + case nano::block_type::open: + case nano::block_type::receive: + return block_op::receive; + case nano::block_type::change: + return block_op::noop; + case nano::block_type::not_a_block: + case nano::block_type::invalid: + release_assert (false); + break; + } + release_assert (false); +} + +bool nano::block_check_context::is_send () const +{ + return op () == block_op::send; +} + +bool nano::block_check_context::is_receive () const +{ + return op () == block_op::receive; +} + +bool nano::block_check_context::is_epoch () const +{ + return op () == block_op::epoch; +} + +nano::amount nano::block_check_context::balance () const +{ + switch (block_m->type ()) + { + case nano::block_type::state: + case nano::block_type::send: + return block_m->balance_field ().value (); + case nano::block_type::open: + return receivable->amount; + case nano::block_type::change: + return previous->balance (); + case nano::block_type::receive: + return previous->balance ().number () + receivable->amount.number (); + default: + release_assert (false); + } +} + +uint64_t nano::block_check_context::height () const +{ + return previous ? previous->sideband ().height + 1 : 1; +} + +nano::epoch nano::block_check_context::epoch () const +{ + if (is_epoch ()) + { + return ledger.constants.epochs.epoch (block_m->link_field ().value ()); + } + nano::epoch account_epoch{ nano::epoch::epoch_0 }; + nano::epoch source_epoch{ nano::epoch::epoch_0 }; + if (previous != nullptr) + { + account_epoch = previous->sideband ().details.epoch; + } + if (receivable.has_value ()) + { + source_epoch = receivable->epoch; + } + return std::max (account_epoch, source_epoch); +} + +nano::amount nano::block_check_context::amount () const +{ + auto balance_l = balance (); + auto previous_balance = previous ? previous->balance () : 0; + switch (op ()) + { + case block_op::receive: + return balance_l.number () - previous_balance.number (); + case block_op::send: + return previous_balance.number () - balance_l.number (); + case block_op::epoch: + case block_op::noop: + release_assert (balance_l.number () == previous_balance.number ()); + return 0; + } +} + +nano::account nano::block_check_context::representative () const +{ + switch (block_m->type ()) + { + case nano::block_type::state: + case nano::block_type::open: + case nano::block_type::change: + return block_m->representative_field ().value (); + case nano::block_type::send: + case nano::block_type::receive: + return state->representative; + default: + release_assert (false); + } +} + +nano::block_hash nano::block_check_context::open () const +{ + if (previous == nullptr) + { + return block_m->hash (); + } + return state->open_block; +} + +bool nano::block_check_context::old () const +{ + return block_m == nullptr; +} + +nano::account nano::block_check_context::account () const +{ + switch (block_m->type ()) + { + case nano::block_type::change: + case nano::block_type::receive: + case nano::block_type::send: + debug_assert (previous != nullptr); + switch (previous->type ()) + { + case nano::block_type::state: + case nano::block_type::open: + return previous->account (); + case nano::block_type::change: + case nano::block_type::receive: + case nano::block_type::send: + return previous->sideband ().account; + case nano::block_type::not_a_block: + case nano::block_type::invalid: + debug_assert (false); + break; + } + break; + case nano::block_type::state: + case nano::block_type::open: + return block_m->account_field ().value (); + case nano::block_type::not_a_block: + case nano::block_type::invalid: + debug_assert (false); + break; + } + // std::unreachable (); c++23 + return 1; // Return an account that cannot be signed for. +} + +nano::block_hash nano::block_check_context::source () const +{ + switch (block_m->type ()) + { + case nano::block_type::send: + case nano::block_type::change: + // 0 is returned for source on send/change blocks + return 0; + case nano::block_type::receive: + case nano::block_type::open: + return block_m->source_field ().value (); + case nano::block_type::state: + return block_m->link_field ().value ().as_block_hash (); + case nano::block_type::not_a_block: + case nano::block_type::invalid: + return 0; + } + debug_assert (false); + return 0; +} + +nano::account nano::block_check_context::signer (nano::epochs const & epochs) const +{ + debug_assert (block_m != nullptr); + switch (block_m->type ()) + { + case nano::block_type::send: + case nano::block_type::receive: + case nano::block_type::change: + debug_assert (previous != nullptr); // Previous block must be passed in for non-open blocks + switch (previous->type ()) + { + case nano::block_type::state: + debug_assert (false && "Legacy blocks can't follow state blocks"); + break; + case nano::block_type::open: + // Open blocks have the account written in the block. + return previous->account (); + default: + // Other legacy block types have the account stored in sideband. + return previous->sideband ().account; + } + break; + case nano::block_type::state: + { + debug_assert (dynamic_cast (block_m.get ())); + // If the block is a send, while the link field may contain an epoch link value, it is actually a malformed destination address. + return (!epochs.is_epoch_link (block_m->link_field ().value ()) || is_send ()) ? block_m->account_field ().value () : epochs.signer (epochs.epoch (block_m->link_field ().value ())); + } + case nano::block_type::open: // Open block signer is determined statelessly as it's written in the block + return block_m->account_field ().value (); + case nano::block_type::invalid: + case nano::block_type::not_a_block: + debug_assert (false); + break; + } + // std::unreachable (); c++23 + return 1; // Return an account that cannot be signed for. +} + +bool nano::block_check_context::gap_previous () const +{ + return !block_m->previous ().is_zero () && previous == nullptr; +} + +bool nano::block_check_context::failed (nano::block_status const & code) const +{ + return code != nano::block_status::progress; +} + +nano::block_status nano::block_check_context::rule_sufficient_work () const +{ + if (ledger.constants.work.difficulty (*block_m) < ledger.constants.work.threshold (block_m->work_version (), details)) + { + return nano::block_status::insufficient_work; + } + return nano::block_status::progress; +} + +nano::block_status nano::block_check_context::rule_reserved_account () const +{ + switch (block_m->type ()) + { + case nano::block_type::open: + case nano::block_type::state: + if (!block_m->account_field ().value ().is_zero ()) + { + return nano::block_status::progress; + } + else + { + return nano::block_status::opened_burn_account; + } + break; + case nano::block_type::change: + case nano::block_type::receive: + case nano::block_type::send: + return nano::block_status::progress; + case nano::block_type::invalid: + case nano::block_type::not_a_block: + release_assert (false); + break; + } + release_assert (false); +} + +nano::block_status nano::block_check_context::rule_previous_frontier () const +{ + debug_assert (block_m != nullptr); // + if (gap_previous ()) + { + return nano::block_status::gap_previous; + } + else + { + return nano::block_status::progress; + } +} + +nano::block_status nano::block_check_context::rule_state_block_account_position () const +{ + if (previous == nullptr) + { + return nano::block_status::progress; + } + switch (block_m->type ()) + { + case nano::block_type::send: + case nano::block_type::receive: + case nano::block_type::change: + { + switch (previous->type ()) + { + case nano::block_type::state: + return nano::block_status::block_position; + default: + return nano::block_status::progress; + } + } + default: + return nano::block_status::progress; + } +} + +nano::block_status nano::block_check_context::rule_state_block_source_position () const +{ + if (!receivable.has_value ()) + { + return nano::block_status::progress; + } + switch (block_m->type ()) + { + case nano::block_type::receive: + case nano::block_type::open: + { + if (receivable->epoch > nano::epoch::epoch_0) + { + return nano::block_status::unreceivable; + } + return nano::block_status::progress; + } + case nano::block_type::state: + return nano::block_status::progress; + default: + release_assert (false); + } +} + +nano::block_status nano::block_check_context::rule_block_signed () const +{ + if (!nano::validate_message (signer (ledger.constants.epochs), block_m->hash (), block_m->block_signature ())) + { + return nano::block_status::progress; + } + return nano::block_status::bad_signature; +} + +nano::block_status nano::block_check_context::rule_metastable () const +{ + debug_assert (state.has_value ()); + if (block_m->previous () == state->head) + { + return nano::block_status::progress; + } + else + { + return nano::block_status::fork; + } +} + +nano::block_status nano::block_check_context::check_receive_rules () const +{ + if (!source_exists) + { + // Probably redundant to check as receivable would also have no value + return nano::block_status::gap_source; + } + if (!receivable.has_value ()) + { + return nano::block_status::unreceivable; + } + if (block_m->type () == nano::block_type::state) + { + auto next_balance = state->balance.number () + receivable->amount.number (); + if (next_balance != balance ().number ()) + { + return nano::block_status::balance_mismatch; + } + } + return nano::block_status::progress; +} + +nano::block_status nano::block_check_context::check_epoch_rules () const +{ + debug_assert (state.has_value ()); + // Epoch blocks may not change an account's balance + if (state->balance != balance ()) + { + return nano::block_status::balance_mismatch; + } + // Epoch blocks may not change an account's representative + if (state->representative != representative ()) + { + return nano::block_status::representative_mismatch; + } + // Epoch blocks may not be created for accounts that have no receivable entries + if (block_m->previous ().is_zero () && !any_receivable) + { + return nano::block_status::gap_epoch_open_pending; + } + auto previous_epoch = nano::epoch::epoch_0; + if (previous != nullptr) + { + previous_epoch = previous->sideband ().details.epoch; + } + // Epoch blocks may only increase epoch number by one + if (!state->head.is_zero () && !nano::epochs::is_sequential (previous_epoch, epoch ())) + { + return nano::block_status::block_position; + } + return nano::block_status::progress; +} + +nano::block_status nano::block_check_context::check_send_rules () const +{ + debug_assert (block_m->type () == nano::block_type::send || block_m->type () == nano::block_type::state); + if (state->balance < balance ()) + { + return nano::block_status::negative_spend; + } + return nano::block_status::progress; +} + +nano::block_status nano::block_check_context::check_noop_rules () const +{ + if (balance () != previous->balance ()) + { + return nano::block_status::balance_mismatch; + } + return nano::block_status::progress; +} + +void nano::block_check_context::load (store::transaction const & transaction) +{ + auto hash = block_m->hash (); + if (ledger.any.exists_or_pruned (transaction, hash)) + { + block_m = nullptr; // Signal this block already exists by nulling out block + return; + } + auto & block = *block_m; + if (!block.previous ().is_zero ()) + { + previous = ledger.any.get (transaction, block.previous ()); + } + if (!gap_previous ()) + { + auto account_l = account (); + auto source_l = source (); + state = ledger.any.get (transaction, account_l); + if (!state) + { + state = nano::account_info{}; + } + source_exists = ledger.any.exists_or_pruned (transaction, source_l); + nano::pending_key key{ account_l, source_l }; + receivable = ledger.any.get (transaction, key); + any_receivable = ledger.any.receivable_any (transaction, account_l); + details = block_details{ epoch (), is_send (), is_receive (), is_epoch () }; + } +} + +nano::block_status nano::block_check_context::check (store::transaction const & transaction) +{ + load (transaction); + if (old ()) + { + return nano::block_status::old; + } + nano::block_status result; + if (failed (result = rule_sufficient_work ())) + { + return result; + } + if (failed (result = rule_reserved_account ())) + { + return result; + } + if (failed (result = rule_previous_frontier ())) + { + return result; + } + if (failed (result = rule_state_block_account_position ())) + { + return result; + } + if (failed (result = rule_state_block_source_position ())) + { + return result; + } + if (failed (result = rule_block_signed ())) + { + return result; + } + if (failed (result = rule_metastable ())) + { + return result; + } + switch (op ()) + { + case block_op::receive: + result = check_receive_rules (); + break; + case block_op::send: + result = check_send_rules (); + break; + case block_op::noop: + result = check_noop_rules (); + break; + case block_op::epoch: + result = check_epoch_rules (); + break; + } + if (result == nano::block_status::progress) + { + nano::block_sideband sideband{ account (), 0, balance (), height (), nano::seconds_since_epoch (), details, receivable ? receivable->epoch : nano::epoch::epoch_0 }; + block_m->sideband_set (sideband); + std::pair, std::optional> receivable; + if (is_send ()) + { + receivable.first = { block_m->destination (), block_m->hash () }; + receivable.second = { account (), amount (), epoch () }; + } + else if (is_receive ()) + { + receivable.first = { block_m->account (), block_m->source () }; + } + std::pair, std::optional> weight; + if (previous != nullptr) + { + weight.first = state->representative; + weight.second = state->balance; + } + nano::account_info info{ block_m->hash (), representative (), open (), balance (), nano::seconds_since_epoch (), height (), epoch () }; + delta = { block_m, info, receivable, weight }; + } + return result; +} diff --git a/nano/secure/block_check_context.hpp b/nano/secure/block_check_context.hpp new file mode 100644 index 0000000000..58153b576f --- /dev/null +++ b/nano/secure/block_check_context.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace nano +{ +class block; +class ledger; +class ledger_constants; +} + +namespace nano::store +{ +class transaction; +} + +namespace nano +{ +class block_check_context +{ + enum class block_op + { + receive, + send, + noop, + epoch + }; + std::shared_ptr block_m; + std::shared_ptr previous; + std::optional state; + std::optional receivable; + bool any_receivable{ false }; + bool source_exists{ false }; + bool failed (nano::block_status const & code) const; + nano::ledger & ledger; + nano::block_details details; + +private: + bool is_send () const; + bool is_receive () const; + bool is_epoch () const; + nano::account account () const; + nano::block_hash source () const; + nano::account signer (nano::epochs const & epochs) const; + bool gap_previous () const; + nano::amount balance () const; + uint64_t height () const; + nano::epoch epoch () const; + nano::amount amount () const; + nano::account representative () const; + nano::block_hash open () const; + +private: // Block checking rules + nano::block_status rule_sufficient_work () const; + /** + Check for account numbers that cannot be used in blocks e.g. account number 0. + */ + nano::block_status rule_reserved_account () const; + /** + This rule checks if the previous block for this block is the head block of the specified account + */ + nano::block_status rule_previous_frontier () const; + + /** + This rule checks that legacy blocks cannot come after state blocks in an account + */ + nano::block_status rule_state_block_account_position () const; + + /** + This rule checks that legacy blocks cannot have a state block as a source + */ + nano::block_status rule_state_block_source_position () const; + nano::block_status rule_block_signed () const; + + /** + This rule identifies metastable blocks (forked blocks) with respect to the ledger and rejects them. + Rejected blocks need to be resolved via consensus + It is assumed that the previous block has already been loaded in to `context' if it exists + Metastable scenarios are: + 1) An initial block arriving for an account that's already been initialized + 2) The previous block exists but it is not the head block + Both of these scenarios can be ifentified by checking: if block->previous () == head + */ + nano::block_status rule_metastable () const; + nano::block_status check_receive_rules () const; + nano::block_status check_epoch_rules () const; + nano::block_status check_send_rules () const; + nano::block_status check_noop_rules () const; + + block_op op () const; + bool old () const; + void load (store::transaction const & transaction); + +public: + block_check_context (nano::ledger & ledger, std::shared_ptr block); + /** + This filters blocks in four directions based on how the link field should be interpreted + For state blocks the link field is interpreted as: + If the balance has decreased, a destination account + If the balance has not decreased + If the link field is 0, a noop + If the link field is an epoch link, an epoch sentinel + Otherwise, a block hash of an block ready to be received + For legacy blocks, the link field interpretation is applied to source field for receive and open blocks or the destination field for send blocks */ + nano::block_status check (store::transaction const & transaction); + std::optional delta; +}; +} // namespace nano