Return per hash vote result (#4510)

* Election use `nano::vote_code`

* Return map of results from `active_transactions::vote (...)`

* Fix test

* Use `to_stat_detail (nano::vote_code)` helper

* Ignore duplicate hashes when processing votes

* Compilation fix

* Fix compilation
This commit is contained in:
Piotr Wójcik 2024-03-21 16:30:21 +01:00 committed by GitHub
commit d2e6f97283
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 121 additions and 109 deletions

View file

@ -583,19 +583,19 @@ TEST (active_transactions, vote_replays)
// First vote is not a replay and confirms the election, second vote should be a replay since the election has confirmed but not yet removed
auto vote_send1 = nano::test::make_final_vote (nano::dev::genesis_key, { send1 });
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote_send1));
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote_send1));
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote_send1).at (send1->hash ()));
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote_send1).at (send1->hash ()));
// Wait until the election is removed, at which point the vote is still a replay since it's been recently confirmed
ASSERT_TIMELY_EQ (5s, node.active.size (), 1);
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote_send1));
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote_send1).at (send1->hash ()));
// Open new account
auto vote_open1 = nano::test::make_final_vote (nano::dev::genesis_key, { open1 });
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote_open1));
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote_open1));
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote_open1).at (open1->hash ()));
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote_open1).at (open1->hash ()));
ASSERT_TIMELY (5s, node.active.empty ());
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote_open1));
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote_open1).at (open1->hash ()));
ASSERT_EQ (nano::Gxrb_ratio, node.ledger.weight (key.pub));
// send 1 raw to key to key
@ -616,27 +616,27 @@ TEST (active_transactions, vote_replays)
// vote2_send2 is a non final vote with little weight, vote1_send2 is the vote that confirms the election
auto vote1_send2 = nano::test::make_final_vote (nano::dev::genesis_key, { send2 });
auto vote2_send2 = nano::test::make_vote (key, { send2 }, 0, 0);
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote2_send2)); // this vote cannot confirm the election
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote2_send2).at (send2->hash ())); // this vote cannot confirm the election
ASSERT_EQ (1, node.active.size ());
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote2_send2)); // this vote cannot confirm the election
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote2_send2).at (send2->hash ())); // this vote cannot confirm the election
ASSERT_EQ (1, node.active.size ());
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote1_send2)); // this vote confirms the election
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote1_send2).at (send2->hash ())); // this vote confirms the election
// this should still return replay, either because the election is still in the AEC or because it is recently confirmed
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote1_send2));
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote1_send2).at (send2->hash ()));
ASSERT_TIMELY (5s, node.active.empty ());
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote1_send2));
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote2_send2));
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote1_send2).at (send2->hash ()));
ASSERT_EQ (nano::vote_code::replay, node.active.vote (vote2_send2).at (send2->hash ()));
// Removing blocks as recently confirmed makes every vote indeterminate
{
nano::lock_guard<nano::mutex> guard (node.active.mutex);
node.active.recently_confirmed.clear ();
}
ASSERT_EQ (nano::vote_code::indeterminate, node.active.vote (vote_send1));
ASSERT_EQ (nano::vote_code::indeterminate, node.active.vote (vote_open1));
ASSERT_EQ (nano::vote_code::indeterminate, node.active.vote (vote1_send2));
ASSERT_EQ (nano::vote_code::indeterminate, node.active.vote (vote2_send2));
ASSERT_EQ (nano::vote_code::indeterminate, node.active.vote (vote_send1).at (send1->hash ()));
ASSERT_EQ (nano::vote_code::indeterminate, node.active.vote (vote_open1).at (open1->hash ()));
ASSERT_EQ (nano::vote_code::indeterminate, node.active.vote (vote1_send2).at (send2->hash ()));
ASSERT_EQ (nano::vote_code::indeterminate, node.active.vote (vote2_send2).at (send2->hash ()));
}
}

View file

@ -72,7 +72,7 @@ TEST (election, quorum_minimum_flip_success)
ASSERT_TIMELY_EQ (5s, election->blocks ().size (), 2);
auto vote = nano::test::make_final_vote (nano::dev::genesis_key, { send2->hash () });
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote));
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote).at (send2->hash ()));
ASSERT_TIMELY (5s, election->confirmed ());
auto const winner = election->winner ();
@ -121,7 +121,7 @@ TEST (election, quorum_minimum_flip_fail)
// genesis generates a final vote for send2 but it should not be enough to reach quorum due to the online_weight_minimum being so high
auto vote = nano::test::make_final_vote (nano::dev::genesis_key, { send2->hash () });
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote));
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote).at (send2->hash ()));
// give the election some time before asserting it is not confirmed so that in case
// it would be wrongfully confirmed, have that immediately fail instead of race
@ -157,7 +157,7 @@ TEST (election, quorum_minimum_confirm_success)
ASSERT_NE (nullptr, election);
ASSERT_EQ (1, election->blocks ().size ());
auto vote = nano::test::make_final_vote (nano::dev::genesis_key, { send1->hash () });
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote));
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote).at (send1->hash ()));
ASSERT_NE (nullptr, node1.block (send1->hash ()));
ASSERT_TIMELY (5s, election->confirmed ());
}
@ -188,7 +188,7 @@ TEST (election, quorum_minimum_confirm_fail)
ASSERT_EQ (1, election->blocks ().size ());
auto vote = nano::test::make_final_vote (nano::dev::genesis_key, { send1->hash () });
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote));
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote).at (send1->hash ()));
// give the election a chance to confirm
WAIT (1s);
@ -251,7 +251,7 @@ TEST (election, quorum_minimum_update_weight_before_quorum_checks)
ASSERT_EQ (1, election->blocks ().size ());
auto vote1 = nano::test::make_final_vote (nano::dev::genesis_key, { send1->hash () });
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote1));
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote1).at (send1->hash ()));
auto channel = node1.network.find_node_id (node2.get_node_id ());
ASSERT_NE (channel, nullptr);
@ -265,7 +265,7 @@ TEST (election, quorum_minimum_update_weight_before_quorum_checks)
// Modify online_m for online_reps to more than is available, this checks that voting below updates it to current online reps.
node1.online_reps.online_m = node_config.online_weight_minimum.number () + 20;
}
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote2));
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote2).at (send1->hash ()));
ASSERT_TIMELY (5s, election->confirmed ());
ASSERT_NE (nullptr, node1.block (send1->hash ()));
}

View file

@ -932,9 +932,9 @@ TEST (votes, add_one)
auto election1 = node1.active.election (send1->qualified_root ());
ASSERT_EQ (1, election1->votes ().size ());
auto vote1 = nano::test::make_vote (nano::dev::genesis_key, { send1 }, nano::vote::timestamp_min * 1, 0);
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote1));
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote1).at (send1->hash ()));
auto vote2 = nano::test::make_vote (nano::dev::genesis_key, { send1 }, nano::vote::timestamp_min * 2, 0);
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote2));
ASSERT_EQ (nano::vote_code::ignored, node1.active.vote (vote2).at (send1->hash ())); // Ignored due to vote cooldown
ASSERT_EQ (2, election1->votes ().size ());
auto votes1 (election1->votes ());
auto existing1 (votes1.find (nano::dev::genesis_key.pub));
@ -973,7 +973,7 @@ TEST (votes, add_existing)
ASSERT_TIMELY (5s, node1.active.election (send1->qualified_root ()));
auto election1 = node1.active.election (send1->qualified_root ());
auto vote1 = nano::test::make_vote (nano::dev::genesis_key, { send1 }, nano::vote::timestamp_min * 1, 0);
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote1));
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote1).at (send1->hash ()));
// Block is already processed from vote
ASSERT_TRUE (node1.active.publish (send1));
ASSERT_EQ (nano::vote::timestamp_min * 1, election1->last_votes[nano::dev::genesis_key.pub].timestamp);
@ -995,13 +995,13 @@ TEST (votes, add_existing)
auto vote_info1 = election1->get_last_vote (nano::dev::genesis_key.pub);
vote_info1.time = std::chrono::steady_clock::now () - std::chrono::seconds (20);
election1->set_last_vote (nano::dev::genesis_key.pub, vote_info1);
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote2));
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote2).at (send2->hash ()));
ASSERT_EQ (nano::vote::timestamp_min * 2, election1->last_votes[nano::dev::genesis_key.pub].timestamp);
// Also resend the old vote, and see if we respect the timestamp
auto vote_info2 = election1->get_last_vote (nano::dev::genesis_key.pub);
vote_info2.time = std::chrono::steady_clock::now () - std::chrono::seconds (20);
election1->set_last_vote (nano::dev::genesis_key.pub, vote_info2);
ASSERT_EQ (nano::vote_code::replay, node1.active.vote (vote1));
ASSERT_EQ (nano::vote_code::replay, node1.active.vote (vote1).at (send1->hash ()));
ASSERT_EQ (nano::vote::timestamp_min * 2, election1->votes ()[nano::dev::genesis_key.pub].timestamp);
auto votes (election1->votes ());
ASSERT_EQ (2, votes.size ());

View file

@ -208,7 +208,7 @@ TEST (vote_processor, no_broadcast_local)
ASSERT_FALSE (node.wallets.reps ().have_half_rep ()); // Genesis balance remaining after `send' is less than the half_rep threshold
// Process a vote with a key that is in the local wallet.
auto vote = std::make_shared<nano::vote> (nano::dev::genesis_key.pub, nano::dev::genesis_key.prv, nano::milliseconds_since_epoch (), nano::vote::duration_max, std::vector<nano::block_hash>{ send->hash () });
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote));
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote).at (send->hash ()));
// Make sure the vote was processed.
auto election (node.active.election (send->qualified_root ()));
ASSERT_NE (nullptr, election);
@ -256,7 +256,7 @@ TEST (vote_processor, local_broadcast_without_a_representative)
node.start_election (send);
// Process a vote without a representative
auto vote = std::make_shared<nano::vote> (nano::dev::genesis_key.pub, nano::dev::genesis_key.prv, nano::milliseconds_since_epoch (), nano::vote::duration_max, std::vector<nano::block_hash>{ send->hash () });
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote));
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote).at (send->hash ()));
// Make sure the vote was processed.
std::shared_ptr<nano::election> election;
ASSERT_TIMELY (5s, election = node.active.election (send->qualified_root ()));
@ -309,7 +309,7 @@ TEST (vote_processor, no_broadcast_local_with_a_principal_representative)
ASSERT_TRUE (node.wallets.reps ().have_half_rep ()); // Genesis balance after `send' is over both half_rep and PR threshold.
// Process a vote with a key that is in the local wallet.
auto vote = std::make_shared<nano::vote> (nano::dev::genesis_key.pub, nano::dev::genesis_key.prv, nano::milliseconds_since_epoch (), nano::vote::duration_max, std::vector<nano::block_hash>{ send->hash () });
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote));
ASSERT_EQ (nano::vote_code::vote, node.active.vote (vote).at (send->hash ()));
// Make sure the vote was processed.
auto election (node.active.election (send->qualified_root ()));
ASSERT_NE (nullptr, election);

View file

@ -84,6 +84,7 @@ enum class detail : uint8_t
none,
success,
unknown,
cache,
// processing queue
queue,
@ -167,12 +168,15 @@ enum class detail : uint8_t
frontier_confirmation_failed,
error_socket_close,
// vote specific
vote_valid,
vote_replay,
vote_indeterminate,
vote_invalid,
// vote result
vote,
valid,
replay,
indeterminate,
// vote processor
vote_overflow,
vote_ignored,
// election specific
vote_new,

View file

@ -438,31 +438,33 @@ nano::election_insertion_result nano::active_transactions::insert (std::shared_p
}
// Validate a vote and apply it to the current election if one exists
nano::vote_code nano::active_transactions::vote (std::shared_ptr<nano::vote> const & vote_a)
std::unordered_map<nano::block_hash, nano::vote_code> nano::active_transactions::vote (std::shared_ptr<nano::vote> const & vote)
{
nano::vote_code result{ nano::vote_code::indeterminate };
// If all hashes were recently confirmed then it is a replay
unsigned recently_confirmed_counter (0);
std::vector<std::pair<std::shared_ptr<nano::election>, nano::block_hash>> process;
std::unordered_map<nano::block_hash, nano::vote_code> results;
std::unordered_map<nano::block_hash, std::shared_ptr<nano::election>> process;
std::vector<nano::block_hash> inactive; // Hashes that should be added to inactive vote cache
{
nano::unique_lock<nano::mutex> lock{ mutex };
for (auto const & hash : vote_a->hashes)
for (auto const & hash : vote->hashes)
{
auto existing (blocks.find (hash));
if (existing != blocks.end ())
// Ignore duplicate hashes (should not happen with a well-behaved voting node)
if (results.find (hash) != results.end ())
{
process.emplace_back (existing->second, hash);
continue;
}
if (auto existing = blocks.find (hash); existing != blocks.end ())
{
process[hash] = existing->second;
}
else if (!recently_confirmed.exists (hash))
{
inactive.emplace_back (hash);
results[hash] = nano::vote_code::indeterminate;
}
else
{
++recently_confirmed_counter;
results[hash] = nano::vote_code::replay;
}
}
}
@ -470,37 +472,38 @@ nano::vote_code nano::active_transactions::vote (std::shared_ptr<nano::vote> con
// Process inactive votes outside of the critical section
for (auto & hash : inactive)
{
add_vote_cache (hash, vote_a);
add_vote_cache (hash, vote);
}
if (!process.empty ())
{
bool replay = false;
bool processed = false;
for (auto const & [election, block_hash] : process)
for (auto const & [block_hash, election] : process)
{
auto const vote_result = election->vote (vote_a->account, vote_a->timestamp (), block_hash);
processed |= (vote_result == nano::election::vote_result::processed);
replay |= (vote_result == nano::election::vote_result::replay);
auto const vote_result = election->vote (vote->account, vote->timestamp (), block_hash);
results[block_hash] = vote_result;
processed |= (vote_result == nano::vote_code::vote);
}
// Republish vote if it is new and the node does not host a principal representative (or close to)
if (processed)
{
auto const reps (node.wallets.reps ());
if (!reps.have_half_rep () && !reps.exists (vote_a->account))
if (!reps.have_half_rep () && !reps.exists (vote->account))
{
node.network.flood_vote (vote_a, 0.5f);
node.network.flood_vote (vote, 0.5f);
}
}
result = replay ? nano::vote_code::replay : nano::vote_code::vote;
}
else if (recently_confirmed_counter == vote_a->hashes.size ())
{
result = nano::vote_code::replay;
}
return result;
// All hashes should have their result set
debug_assert (std::all_of (vote->hashes.begin (), vote->hashes.end (), [&results] (auto const & hash) {
return results.find (hash) != results.end ();
}));
return results;
}
bool nano::active_transactions::active (nano::qualified_root const & root_a) const

View file

@ -152,7 +152,7 @@ public:
*/
nano::election_insertion_result insert (std::shared_ptr<nano::block> const &, nano::election_behavior = nano::election_behavior::normal);
// Distinguishes replay votes, cannot be determined if the block is not in any election
nano::vote_code vote (std::shared_ptr<nano::vote> const &);
std::unordered_map<nano::block_hash, nano::vote_code> vote (std::shared_ptr<nano::vote> const &);
// Is the root of this block in the roots container
bool active (nano::block const &) const;
bool active (nano::qualified_root const &) const;

View file

@ -434,12 +434,12 @@ std::shared_ptr<nano::block> nano::election::find (nano::block_hash const & hash
return result;
}
auto nano::election::vote (nano::account const & rep, uint64_t timestamp_a, nano::block_hash const & block_hash_a, vote_source vote_source_a) -> vote_result
nano::vote_code nano::election::vote (nano::account const & rep, uint64_t timestamp_a, nano::block_hash const & block_hash_a, nano::vote_source vote_source_a)
{
auto weight = node.ledger.weight (rep);
if (!node.network_params.network.is_dev_network () && weight <= node.minimum_principal_weight ())
{
return vote_result::ignored;
return vote_code::indeterminate;
}
nano::unique_lock<nano::mutex> lock{ mutex };
@ -450,11 +450,11 @@ auto nano::election::vote (nano::account const & rep, uint64_t timestamp_a, nano
auto last_vote_l (last_vote_it->second);
if (last_vote_l.timestamp > timestamp_a)
{
return vote_result::replay;
return vote_code::replay;
}
if (last_vote_l.timestamp == timestamp_a && !(last_vote_l.hash < block_hash_a))
{
return vote_result::replay;
return vote_code::replay;
}
auto max_vote = timestamp_a == std::numeric_limits<uint64_t>::max () && last_vote_l.timestamp < timestamp_a;
@ -468,7 +468,7 @@ auto nano::election::vote (nano::account const & rep, uint64_t timestamp_a, nano
if (!max_vote && !past_cooldown)
{
return vote_result::ignored;
return vote_code::ignored;
}
}
@ -494,7 +494,7 @@ auto nano::election::vote (nano::account const & rep, uint64_t timestamp_a, nano
confirm_if_quorum (lock);
}
return vote_result::processed;
return vote_code::vote;
}
bool nano::election::publish (std::shared_ptr<nano::block> const & block_a)

View file

@ -6,6 +6,7 @@
#include <nano/node/election_behavior.hpp>
#include <nano/node/election_status.hpp>
#include <nano/node/vote_with_weight_info.hpp>
#include <nano/secure/common.hpp>
#include <atomic>
#include <chrono>
@ -42,24 +43,10 @@ struct election_extended_status final
void operator() (nano::object_stream &) const;
};
class election final : public std::enable_shared_from_this<nano::election>
class election final : public std::enable_shared_from_this<election>
{
nano::id_t const id{ nano::next_id () }; // Track individual objects when tracing
public:
enum class vote_source
{
live,
cache,
};
enum class vote_result
{
ignored,
processed,
replay,
};
private:
// Minimum time between broadcasts of the current winner of an election, as a backup to requesting confirmations
std::chrono::milliseconds base_latency () const;
@ -117,7 +104,7 @@ public: // Interface
* Process vote. Internally uses cooldown to throttle non-final votes
* If the election reaches consensus, it will be confirmed
*/
vote_result vote (nano::account const & representative, uint64_t timestamp, nano::block_hash const & block_hash, vote_source = vote_source::live);
nano::vote_code vote (nano::account const & representative, uint64_t timestamp, nano::block_hash const & block_hash, nano::vote_source = nano::vote_source::live);
bool publish (std::shared_ptr<nano::block> const & block_a);
// Confirm this block if quorum is met
void confirm_if_quorum (nano::unique_lock<nano::mutex> &);

View file

@ -69,8 +69,8 @@ std::size_t nano::vote_cache::entry::fill (std::shared_ptr<nano::election> const
std::size_t inserted = 0;
for (const auto & entry : voters_m)
{
auto result = election->vote (entry.representative, entry.timestamp, hash_m, nano::election::vote_source::cache);
if (result == nano::election::vote_result::processed)
auto result = election->vote (entry.representative, entry.timestamp, hash_m, nano::vote_source::cache);
if (result == nano::vote_code::vote)
{
inserted++;
}

View file

@ -163,32 +163,25 @@ void nano::vote_processor::verify_votes (decltype (votes) const & votes_a)
nano::vote_code nano::vote_processor::vote_blocking (std::shared_ptr<nano::vote> const & vote_a, std::shared_ptr<nano::transport::channel> const & channel_a, bool validated)
{
auto result (nano::vote_code::invalid);
auto result = nano::vote_code::invalid;
if (validated || !vote_a->validate ())
{
result = active.vote (vote_a);
auto vote_results = active.vote (vote_a);
// Aggregate results for individual hashes
bool replay = false;
bool processed = false;
for (auto const & [hash, hash_result] : vote_results)
{
replay |= (hash_result == nano::vote_code::replay);
processed |= (hash_result == nano::vote_code::vote);
}
result = replay ? nano::vote_code::replay : (processed ? nano::vote_code::vote : nano::vote_code::indeterminate);
observers.vote.notify (vote_a, channel_a, result);
}
std::string status;
switch (result)
{
case nano::vote_code::invalid:
status = "Invalid";
stats.inc (nano::stat::type::vote, nano::stat::detail::vote_invalid);
break;
case nano::vote_code::replay:
status = "Replay";
stats.inc (nano::stat::type::vote, nano::stat::detail::vote_replay);
break;
case nano::vote_code::vote:
status = "Vote";
stats.inc (nano::stat::type::vote, nano::stat::detail::vote_valid);
break;
case nano::vote_code::indeterminate:
status = "Indeterminate";
stats.inc (nano::stat::type::vote, nano::stat::detail::vote_indeterminate);
break;
}
stats.inc (nano::stat::type::vote, to_stat_detail (result));
logger.trace (nano::log::type::vote_processor, nano::log::detail::vote_processed,
nano::log::arg{ "vote", vote_a },

View file

@ -341,6 +341,20 @@ nano::block_hash const & nano::unchecked_key::key () const
return previous;
}
nano::stat::detail nano::to_stat_detail (nano::vote_code code)
{
auto value = magic_enum::enum_cast<nano::stat::detail> (magic_enum::enum_name (code));
debug_assert (value);
return value.value_or (nano::stat::detail{});
}
nano::stat::detail nano::to_stat_detail (nano::vote_source source)
{
auto value = magic_enum::enum_cast<nano::stat::detail> (magic_enum::enum_name (source));
debug_assert (value);
return value.value_or (nano::stat::detail{});
}
std::string_view nano::to_string (nano::block_status code)
{
return magic_enum::enum_name (code);

View file

@ -191,9 +191,20 @@ enum class vote_code
invalid, // Vote is not signed correctly
replay, // Vote does not have the highest timestamp, it's a replay
vote, // Vote has the highest timestamp
indeterminate // Unknown if replay or vote
indeterminate, // Unknown if replay or vote
ignored, // Vote is valid, but got ingored (e.g. due to cooldown)
};
nano::stat::detail to_stat_detail (vote_code);
enum class vote_source
{
live,
cache,
};
nano::stat::detail to_stat_detail (vote_source);
enum class block_status
{
progress, // Hasn't been seen before, signed correctly