diff --git a/nano/secure/CMakeLists.txt b/nano/secure/CMakeLists.txt index f80ac9d8b3..ea2830e3b3 100644 --- a/nano/secure/CMakeLists.txt +++ b/nano/secure/CMakeLists.txt @@ -39,6 +39,8 @@ add_library( ${PLATFORM_SECURE_SOURCE} ${CMAKE_BINARY_DIR}/bootstrap_weights_live.cpp ${CMAKE_BINARY_DIR}/bootstrap_weights_beta.cpp + block_check_context.cpp + block_check_context.hpp common.hpp common.cpp ledger.hpp diff --git a/nano/secure/block_check_context.cpp b/nano/secure/block_check_context.cpp new file mode 100644 index 0000000000..08b994d3a7 --- /dev/null +++ b/nano/secure/block_check_context.cpp @@ -0,0 +1,525 @@ +#include +#include +#include +#include +#include +#include + +nano::block_check_context::block_check_context (nano::store::transaction const & transaction, nano::ledger & ledger, nano::block & block) : + block_m{ &block }, + ledger{ ledger } +{ + if (ledger.block_or_pruned_exists (transaction, block.hash ())) + { + block_m = nullptr; // Signal this block already exists by nulling out block + return; + } + if (!block.previous ().is_zero ()) + { + previous = ledger.store.block.get (transaction, block.previous ()); + } + if (!gap_previous ()) + { + state = ledger.account_info (transaction, account ()); + if (!state) + { + state = nano::account_info{}; + } + source_exists = ledger.block_or_pruned_exists (transaction, source ()); + receivable = ledger.pending_info (transaction, { account (), source () }); + any_receivable = ledger.store.pending.any (transaction, account ()); + details = block_details{ epoch (), is_send (), is_receive (), is_epoch () }; + } +} + +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 () < state->balance) + { + return block_op::send; + } + if (previous != nullptr && block_m->link ().is_zero ()) + { + return block_op::noop; + } + if (ledger.constants.epochs.is_epoch_link (block_m->link ())) + { + 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 (); + case nano::block_type::open: + return receivable->amount; + case nano::block_type::change: + return ledger.balance (*previous); + case nano::block_type::receive: + return ledger.balance (*previous) + 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 ()); + } + 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 ? ledger.balance (*previous) : 0; + switch (op ()) + { + case block_op::receive: + return balance_l.number () - previous_balance; + case block_op::send: + return previous_balance - balance_l.number (); + case block_op::epoch: + case block_op::noop: + release_assert (balance_l.number () == previous_balance); + 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 (); + 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 (); + 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 + case nano::block_type::receive: + case nano::block_type::open: + return block_m->source (); + case nano::block_type::state: + return block_m->link ().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)); + // 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 ()) || is_send ()) ? block_m->account () : epochs.signer (epochs.epoch (block_m->link ())); + } + case nano::block_type::open: // Open block signer is determined statelessly as it's written in the block + return block_m->account (); + 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::ledger_code const & code) const +{ + return code != nano::ledger_code::progress; +} + +nano::ledger_code 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::ledger_code::insufficient_work; + } + return nano::ledger_code::progress; +} + +nano::ledger_code 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 ().is_zero ()) + { + return nano::ledger_code::progress; + } + else + { + return nano::ledger_code::opened_burn_account; + } + break; + case nano::block_type::change: + case nano::block_type::receive: + case nano::block_type::send: + return nano::ledger_code::progress; + case nano::block_type::invalid: + case nano::block_type::not_a_block: + release_assert (false); + break; + } + release_assert (false); +} + +nano::ledger_code nano::block_check_context::rule_previous_frontier () const +{ + debug_assert (block_m != nullptr); // + if (gap_previous ()) + { + return nano::ledger_code::gap_previous; + } + else + { + return nano::ledger_code::progress; + } +} + +nano::ledger_code nano::block_check_context::rule_state_block_account_position () const +{ + if (previous == nullptr) + { + return nano::ledger_code::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::ledger_code::block_position; + default: + return nano::ledger_code::progress; + } + } + default: + return nano::ledger_code::progress; + } +} + +nano::ledger_code nano::block_check_context::rule_state_block_source_position () const +{ + if (!receivable.has_value ()) + { + return nano::ledger_code::progress; + } + switch (block_m->type ()) + { + case nano::block_type::receive: + case nano::block_type::open: + { + if (receivable->epoch > nano::epoch::epoch_0) + { + return nano::ledger_code::unreceivable; + } + return nano::ledger_code::progress; + } + case nano::block_type::state: + return nano::ledger_code::progress; + default: + release_assert (false); + } +} + +nano::ledger_code 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::ledger_code::progress; + } + return nano::ledger_code::bad_signature; +} + +nano::ledger_code nano::block_check_context::rule_metastable () const +{ + debug_assert (state.has_value ()); + if (block_m->previous () == state->head) + { + return nano::ledger_code::progress; + } + else + { + return nano::ledger_code::fork; + } +} + +nano::ledger_code nano::block_check_context::check_receive_rules () const +{ + if (!source_exists) + { + // Probably redundant to check as receivable would also have no value + return nano::ledger_code::gap_source; + } + if (!receivable.has_value ()) + { + return nano::ledger_code::unreceivable; + } + if (block_m->type () == nano::block_type::state) + { + auto next_balance = state->balance.number () + receivable->amount.number (); + if (next_balance != block_m->balance ().number ()) + { + return nano::ledger_code::balance_mismatch; + } + } + return nano::ledger_code::progress; +} + +nano::ledger_code 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 != block_m->balance ()) + { + return nano::ledger_code::balance_mismatch; + } + // Epoch blocks may not change an account's representative + if (state->representative != representative ()) + { + return nano::ledger_code::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::ledger_code::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::ledger_code::block_position; + } + return nano::ledger_code::progress; +} + +nano::ledger_code 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 < block_m->balance ()) + { + return nano::ledger_code::negative_spend; + } + return nano::ledger_code::progress; +} + +nano::ledger_code nano::block_check_context::check_noop_rules () const +{ + if (balance () != ledger.balance (*previous)) + { + return nano::ledger_code::balance_mismatch; + } + return nano::ledger_code::progress; +} + +nano::ledger_code nano::block_check_context::check () +{ + if (old ()) + { + return nano::ledger_code::old; + } + nano::ledger_code 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::ledger_code::progress) + { + nano::block_sideband sideband{ account (), representative (), balance (), amount (), height (), nano::seconds_since_epoch (), details, receivable ? receivable->epoch : nano::epoch::epoch_0 }; + block_m->sideband_set (sideband); + } + return result; +} diff --git a/nano/secure/block_check_context.hpp b/nano/secure/block_check_context.hpp new file mode 100644 index 0000000000..62791014a5 --- /dev/null +++ b/nano/secure/block_check_context.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace nano +{ +class block; +class ledger; +} + +namespace nano::store +{ +class transaction; +} + +namespace nano +{ +class block_check_context +{ + enum class block_op + { + receive, + send, + noop, + epoch + }; + nano::block * block_m; + std::shared_ptr previous; + std::optional state; + std::optional receivable; + bool any_receivable{ false }; + bool source_exists{ false }; + bool failed (nano::ledger_code 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::ledger_code rule_sufficient_work () const; + /** + Check for account numbers that cannot be used in blocks e.g. account number 0. + */ + nano::ledger_code rule_reserved_account () const; + /** + This rule checks if the previous block for this block is the head block of the specified account + */ + nano::ledger_code rule_previous_frontier () const; + + /** + This rule checks that legacy blocks cannot come after state blocks in an account + */ + nano::ledger_code rule_state_block_account_position () const; + + /** + This rule checks that legacy blocks cannot have a state block as a source + */ + nano::ledger_code rule_state_block_source_position () const; + nano::ledger_code 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::ledger_code rule_metastable () const; + nano::ledger_code check_receive_rules () const; + nano::ledger_code check_epoch_rules () const; + nano::ledger_code check_send_rules () const; + nano::ledger_code check_noop_rules () const; + +public: + block_check_context (nano::store::transaction const & transaction, nano::ledger & ledger, nano::block & 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 */ + block_op op () const; + bool old () const; + nano::ledger_code check (); +}; +} // namespace nano diff --git a/nano/secure/ledger.cpp b/nano/secure/ledger.cpp index f2e3aa3792..acb2a2bb05 100644 --- a/nano/secure/ledger.cpp +++ b/nano/secure/ledger.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -321,7 +322,6 @@ void ledger_processor::state_block_impl (nano::state_block & block_a) if (result == nano::ledger_code::progress) { ledger.stats.inc (nano::stat::type::ledger, nano::stat::detail::state_block); - block_a.sideband_set (nano::block_sideband (block_a.hashables.account /* unused */, 0 /* unused */, 0 /* unused */, amount, info.block_count + 1, nano::seconds_since_epoch (), block_details, source_epoch)); ledger.store.block.put (transaction, hash, block_a); if (!block_a.hashables.previous.is_zero ()) { @@ -417,7 +417,6 @@ void ledger_processor::epoch_block_impl (nano::state_block & block_a) if (result == nano::ledger_code::progress) { ledger.stats.inc (nano::stat::type::ledger, nano::stat::detail::epoch_block); - block_a.sideband_set (nano::block_sideband (block_a.hashables.account /* unused */, 0 /* unused */, 0 /* unused */, 0, info.block_count + 1, nano::seconds_since_epoch (), block_details, nano::epoch::epoch_0 /* unused */)); ledger.store.block.put (transaction, hash, block_a); ledger.store.account.put (transaction, ledger.account (block_a), ledger.account_info (transaction, block_a)); if (!block_a.hashables.previous.is_zero ()) @@ -463,7 +462,6 @@ void ledger_processor::change_block (nano::change_block & block_a) if (result == nano::ledger_code::progress) { debug_assert (!validate_message (account, hash, block_a.signature)); - block_a.sideband_set (nano::block_sideband (account, 0 /* unused */, info->balance, 0, info->block_count + 1, nano::seconds_since_epoch (), block_details, nano::epoch::epoch_0 /* unused */)); ledger.store.block.put (transaction, hash, block_a); ledger.store.successor.put (transaction, block_a.hashables.previous, hash); auto balance (ledger.balance (transaction, block_a.hashables.previous)); @@ -512,7 +510,6 @@ void ledger_processor::send_block (nano::send_block & block_a) { auto amount (info->balance.number () - block_a.hashables.balance.number ()); ledger.cache.rep_weights.representation_add (info->representative, 0 - amount); - block_a.sideband_set (nano::block_sideband (account, info->representative, block_a.hashables.balance /* unused */, amount, info->block_count + 1, nano::seconds_since_epoch (), block_details, nano::epoch::epoch_0 /* unused */)); ledger.store.block.put (transaction, hash, block_a); ledger.store.successor.put (transaction, block_a.hashables.previous, hash); ledger.store.account.put (transaction, ledger.account (block_a), ledger.account_info (transaction, block_a)); @@ -562,7 +559,7 @@ void ledger_processor::receive_block (nano::receive_block & block_a) result = ledger.store.pending.get (transaction, key, pending) ? nano::ledger_code::unreceivable : nano::ledger_code::progress; // Has this source already been received (Malformed) if (result == nano::ledger_code::progress) { - result = pending.epoch == nano::epoch::epoch_0 ? nano::ledger_code::progress : nano::ledger_code::unreceivable; // Are we receiving a state-only send? (Malformed) + result = pending.epoch == nano::epoch::epoch_0 ? nano::ledger_code::progress : nano::ledger_code::block_position; // Are we receiving a state-only send? (Malformed) if (result == nano::ledger_code::progress) { nano::block_details block_details (nano::epoch::epoch_0, false /* unused */, false /* unused */, false /* unused */); @@ -578,7 +575,6 @@ void ledger_processor::receive_block (nano::receive_block & block_a) } #endif ledger.store.pending.del (transaction, key); - block_a.sideband_set (nano::block_sideband (account, info->representative, new_balance, pending.amount, info->block_count + 1, nano::seconds_since_epoch (), block_details, nano::epoch::epoch_0 /* unused */)); ledger.store.block.put (transaction, hash, block_a); ledger.store.successor.put (transaction, block_a.hashables.previous, hash); ledger.store.account.put (transaction, ledger.account (block_a), ledger.account_info (transaction, block_a)); @@ -642,7 +638,6 @@ void ledger_processor::open_block (nano::open_block & block_a) } #endif ledger.store.pending.del (transaction, key); - block_a.sideband_set (nano::block_sideband (block_a.hashables.account, 0 /* unused */, pending.amount, pending.amount, 1, nano::seconds_since_epoch (), block_details, nano::epoch::epoch_0 /* unused */)); ledger.store.block.put (transaction, hash, block_a); ledger.store.account.put (transaction, ledger.account (block_a), ledger.account_info (transaction, block_a)); ledger.cache.rep_weights.representation_add (block_a.representative (), pending.amount.number ()); @@ -829,13 +824,18 @@ std::optional nano::ledger::pending_info (store::transaction nano::ledger_code nano::ledger::process (store::write_transaction const & transaction_a, std::shared_ptr block_a) { debug_assert (!constants.work.validate_entry (*block_a) || constants.genesis == nano::dev::genesis); - ledger_processor processor (*this, transaction_a); - block_a->visit (processor); - if (processor.result == nano::ledger_code::progress) - { + nano::block_check_context ctx{ transaction_a, *this, *block_a }; + auto code = ctx.check (); + if (code == nano::ledger_code::progress) + { + debug_assert (block_a->has_sideband ()); + ledger_processor processor (*this, transaction_a); + block_a->visit (processor); + debug_assert (processor.result == nano::ledger_code::progress); ++cache.block_count; + return processor.result; } - return processor.result; + return code; } nano::account nano::ledger::representative (nano::block const & block)