Changes the semantics of election::confirmed to return if the block is durably confirmed on disk, rather than simply being confirmed in memory. This was causing subtle race conditions or unnecessary extra checking to ensure the confirmation has been committed to disk. (#4200)

Changes the semantics of election::confirmed to return if the block is durably confirmed on disk, rather than simply being confirmed in memory. This was causing subtle race conditions or unnecessary extra checking to ensure the confirmation has been committed to disk.

Modifies tests:
active_transactions.dropped_cleanup
active_transactions.inactive_votes_cache_election_start
active_transactions.republish_winner
confirmation_height.conflict_rollback_cemented
election.quorum_minimum_confirm_success
election.quorum_minimum_update_weight_before_quorum_checks
node.rollback_gap_source
rpc.confirmation_active
vote_processor.invalid_signature

Rewriting test to use a block that is not confirmed on disk.
Cleaning up confirmation_height.conflict_rollback_cemented and reduce complexity.
Cleaning up election.quorum_minimum_confirm_success and fixing race condition where confirmation happens asynchronously.
Fixing race condition in election.quorum_minimum_update_weight_before_quorum_checks when checking asynchronous election confirmation.
Cleaning up and simplifying node.rollback_gap_source
Removing some test calls to block_processor::flush which is being phased out.
This commit is contained in:
clemahieu 2023-03-28 16:13:04 +01:00 committed by GitHub
commit fde815f3ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 105 additions and 162 deletions

View file

@ -513,15 +513,11 @@ TEST (active_transactions, inactive_votes_cache_election_start)
ASSERT_TRUE (send4_cache);
ASSERT_EQ (3, send4_cache->voters.size ());
node.process_active (send3);
node.block_processor.flush ();
// An election is started for send6 but does not confirm
ASSERT_TIMELY (5s, 1 == node.active.size ());
node.vote_processor.flush ();
// An election is started for send6 but does not
ASSERT_FALSE (node.block_confirmed_or_being_confirmed (send3->hash ()));
// send7 cannot be voted on but an election should be started from inactive votes
ASSERT_FALSE (node.ledger.dependents_confirmed (node.store.tx_begin_read (), *send4));
node.process_active (send4);
node.block_processor.flush ();
ASSERT_TIMELY (5s, 7 == node.ledger.cache.cemented_count);
}
@ -622,26 +618,28 @@ TEST (active_transactions, dropped_cleanup)
nano::node_flags flags;
flags.disable_request_loop = true;
auto & node (*system.add_node (flags));
auto chain = nano::test::setup_chain (system, node, 1, nano::dev::genesis_key, false);
auto hash = chain[0]->hash ();
// Add to network filter to ensure proper cleanup after the election is dropped
std::vector<uint8_t> block_bytes;
{
nano::vectorstream stream (block_bytes);
nano::dev::genesis->serialize (stream);
chain[0]->serialize (stream);
}
ASSERT_FALSE (node.network.publish_filter.apply (block_bytes.data (), block_bytes.size ()));
ASSERT_TRUE (node.network.publish_filter.apply (block_bytes.data (), block_bytes.size ()));
auto election = nano::test::start_election (system, node, nano::dev::genesis->hash ());
auto election = nano::test::start_election (system, node, hash);
ASSERT_NE (nullptr, election);
// Not yet removed
ASSERT_TRUE (node.network.publish_filter.apply (block_bytes.data (), block_bytes.size ()));
ASSERT_TRUE (node.active.active (nano::dev::genesis->hash ()));
ASSERT_TRUE (node.active.active (hash));
// Now simulate dropping the election
ASSERT_FALSE (election->confirmed ());
node.active.erase (*nano::dev::genesis);
node.active.erase (*chain[0]);
// The filter must have been cleared
ASSERT_FALSE (node.network.publish_filter.apply (block_bytes.data (), block_bytes.size ()));
@ -650,16 +648,16 @@ TEST (active_transactions, dropped_cleanup)
ASSERT_EQ (1, node.stats.count (nano::stat::type::active_dropped, nano::stat::detail::normal));
// Block cleared from active
ASSERT_FALSE (node.active.active (nano::dev::genesis->hash ()));
ASSERT_FALSE (node.active.active (hash));
// Repeat test for a confirmed election
ASSERT_TRUE (node.network.publish_filter.apply (block_bytes.data (), block_bytes.size ()));
election = nano::test::start_election (system, node, nano::dev::genesis->hash ());
election = nano::test::start_election (system, node, hash);
ASSERT_NE (nullptr, election);
election->force_confirm ();
ASSERT_TRUE (election->confirmed ());
node.active.erase (*nano::dev::genesis);
ASSERT_TIMELY (5s, election->confirmed ());
node.active.erase (*chain[0]);
// The filter should not have been cleared
ASSERT_TRUE (node.network.publish_filter.apply (block_bytes.data (), block_bytes.size ()));
@ -668,7 +666,7 @@ TEST (active_transactions, dropped_cleanup)
ASSERT_EQ (1, node.stats.count (nano::stat::type::active_dropped, nano::stat::detail::normal));
// Block cleared from active
ASSERT_FALSE (node.active.active (nano::dev::genesis->hash ()));
ASSERT_FALSE (node.active.active (hash));
}
TEST (active_transactions, republish_winner)

View file

@ -1092,98 +1092,53 @@ TEST (confirmation_height, all_block_types)
test_mode (nano::confirmation_height_mode::unbounded);
}
// this test cements a block on one node and another block on another node
// it therefore tests that once a block is confirmed it cannot be rolled back
// and if both nodes have different branches of the fork cemented then it is a permanent fork
// This test ensures a block that's cemented cannot be rolled back by the node
// A block is inserted and confirmed then later a different block is force inserted with a rollback attempt
TEST (confirmation_height, conflict_rollback_cemented)
{
// functor to perform the conflict_rollback_cemented test using a certain mode
auto test_mode = [] (nano::confirmation_height_mode mode_a) {
nano::state_block_builder builder{};
nano::state_block_builder builder;
auto const genesis_hash = nano::dev::genesis->hash ();
nano::test::system system{};
nano::node_flags node_flags{};
nano::test::system system;
nano::node_flags node_flags;
node_flags.confirmation_height_processor_mode = mode_a;
// create node 1 and account key1 (no voting key yet)
auto node1 = system.add_node (node_flags);
nano::keypair key1{};
nano::keypair key1;
// create one side of a forked transaction on node1
auto send1 = builder.make_block ()
.previous (genesis_hash)
.account (nano::dev::genesis_key.pub)
.representative (nano::dev::genesis_key.pub)
.link (key1.pub)
.balance (nano::dev::constants.genesis_amount - 100)
.sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub)
.work (*system.work.generate (genesis_hash))
.build_shared ();
node1->process_active (send1);
ASSERT_TIMELY (5s, node1->active.election (send1->qualified_root ()) != nullptr);
auto fork1a = builder.make_block ()
.previous (genesis_hash)
.account (nano::dev::genesis_key.pub)
.representative (nano::dev::genesis_key.pub)
.link (key1.pub)
.balance (nano::dev::constants.genesis_amount - 100)
.sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub)
.work (*system.work.generate (genesis_hash))
.build_shared ();
ASSERT_EQ (nano::process_result::progress, node1->process (*fork1a).code);
ASSERT_TRUE (nano::test::confirm (*node1, { fork1a }));
ASSERT_TIMELY (5s, nano::test::confirmed (*node1, { fork1a }));
// create the other side of the fork on node2
nano::keypair key2;
auto send2 = builder.make_block ()
.previous (genesis_hash)
.account (nano::dev::genesis_key.pub)
.representative (nano::dev::genesis_key.pub)
.link (key2.pub)
.balance (nano::dev::constants.genesis_amount - 100)
.sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub)
.work (*system.work.generate (genesis_hash))
.build_shared ();
// create node2, with send2 pre-initialised in the ledger so that block send1 cannot possibly get in the ledger first
system.initialization_blocks.push_back (send2);
auto node2 = system.add_node (node_flags);
system.initialization_blocks.clear ();
auto wallet1 = system.wallet (0);
node2->process_active (send2);
ASSERT_TIMELY (5s, node2->active.election (send2->qualified_root ()) != nullptr);
// force confirm send2 on node2
ASSERT_TIMELY (5s, node2->ledger.store.block.get (node2->ledger.store.tx_begin_read (), send2->hash ()));
node2->process_confirmed (nano::election_status{ send2 });
ASSERT_TIMELY (5s, node2->block_confirmed (send2->hash ()));
// make node1 a voting node (it has all the voting weight)
// from now on, node1 can vote for send1 at any time
wallet1->insert_adhoc (nano::dev::genesis_key.prv);
// we expect node1 to vote for one side of the fork only, whichever side
std::shared_ptr<nano::election> election_send1_node1{};
ASSERT_EQ (send1->qualified_root (), send2->qualified_root ());
ASSERT_TIMELY (5s, (election_send1_node1 = node1->active.election (send1->qualified_root ())) != nullptr);
ASSERT_TIMELY (5s, 2 == election_send1_node1->votes ().size ());
// check that the send1 on node1 won the election and got confirmed
// this happens because send1 is seen first by node1, and therefore it already winning and it cannot replaced by send2
ASSERT_TIMELY (5s, election_send1_node1->confirmed ());
auto const winner = election_send1_node1->winner ();
ASSERT_NE (nullptr, winner);
ASSERT_EQ (*winner, *send1);
auto fork1b = builder.make_block ()
.previous (genesis_hash)
.account (nano::dev::genesis_key.pub)
.representative (nano::dev::genesis_key.pub)
.link (key2.pub) // Different destination same 'previous'
.balance (nano::dev::constants.genesis_amount - 100)
.sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub)
.work (*system.work.generate (genesis_hash))
.build_shared ();
node1->block_processor.force (fork1b);
// node2 already has send2 forced confirmed whilst node1 should have confirmed send1 and therefore we have a cemented fork on node2
// and node2 should print an error message on the log that it cannot rollback send2 because it is already cemented
ASSERT_TIMELY (5s, 1 == node2->stats.count (nano::stat::type::ledger, nano::stat::detail::rollback_failed));
// get the tally for election the election on node1
// we expect the winner to be send1 and we expect send1 to have "genesis balance" vote weight
auto const tally = election_send1_node1->tally ();
ASSERT_FALSE (tally.empty ());
auto const & [amount, winner_alias] = *tally.begin ();
ASSERT_EQ (*winner_alias, *send1);
ASSERT_EQ (amount, nano::dev::constants.genesis_amount - 100);
// we expect send1 to exist on node1, is that because send2 is rolled back?
ASSERT_TRUE (node1->ledger.block_or_pruned_exists (send1->hash ()));
ASSERT_FALSE (node1->ledger.block_or_pruned_exists (send2->hash ()));
// we expect only send2 to be existing on node2
ASSERT_TRUE (node2->ledger.block_or_pruned_exists (send2->hash ()));
ASSERT_FALSE (node2->ledger.block_or_pruned_exists (send1->hash ()));
[[maybe_unused]] size_t count = 0;
ASSERT_TIMELY (5s, 1 == (count = node1->stats.count (nano::stat::type::ledger, nano::stat::detail::rollback_failed)));
ASSERT_TRUE (nano::test::confirmed (*node1, { fork1a->hash () })); // fork1a should still remain after the rollback failed event
};
test_mode (nano::confirmation_height_mode::bounded);

View file

@ -1,4 +1,5 @@
#include <nano/node/election.hpp>
#include <nano/test_common/chains.hpp>
#include <nano/test_common/system.hpp>
#include <nano/test_common/testutil.hpp>
@ -19,7 +20,8 @@ TEST (election, construction)
TEST (election, behavior)
{
nano::test::system system (1);
auto election = nano::test::start_election (system, *system.nodes[0], nano::dev::genesis->hash ());
auto chain = nano::test::setup_chain (system, *system.nodes[0], 1, nano::dev::genesis_key, false);
auto election = nano::test::start_election (system, *system.nodes[0], chain[0]->hash ());
ASSERT_NE (nullptr, election);
ASSERT_EQ (nano::election_behavior::normal, election->behavior ());
}
@ -125,10 +127,11 @@ TEST (election, quorum_minimum_flip_fail)
ASSERT_FALSE (node.block_confirmed (send2->hash ()));
}
// This test ensures blocks can be confirmed precisely at the quorum minimum
TEST (election, quorum_minimum_confirm_success)
{
nano::test::system system;
nano::node_config node_config (nano::test::get_available_port (), system.logging);
nano::node_config node_config{ nano::test::get_available_port (), system.logging };
node_config.online_weight_minimum = nano::dev::constants.genesis_amount;
node_config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled;
auto & node1 = *system.add_node (node_config);
@ -138,14 +141,13 @@ TEST (election, quorum_minimum_confirm_success)
.account (nano::dev::genesis_key.pub)
.previous (nano::dev::genesis->hash ())
.representative (nano::dev::genesis_key.pub)
.balance (node1.online_reps.delta ())
.balance (node1.online_reps.delta ()) // Only minimum quorum remains
.link (key1.pub)
.work (0)
.sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub)
.build_shared ();
node1.work_generate_blocking (*send1);
node1.process_active (send1);
node1.block_processor.flush ();
node1.scheduler.activate (nano::dev::genesis_key.pub, node1.store.tx_begin_read ());
ASSERT_TIMELY (5s, node1.active.election (send1->qualified_root ()));
auto election = node1.active.election (send1->qualified_root ());
@ -153,9 +155,8 @@ TEST (election, quorum_minimum_confirm_success)
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));
node1.block_processor.flush ();
ASSERT_NE (nullptr, node1.block (send1->hash ()));
ASSERT_TRUE (election->confirmed ());
ASSERT_TIMELY (5s, election->confirmed ());
}
// checks that block cannot be confirmed if there is no enough votes to reach quorum
@ -199,7 +200,7 @@ namespace nano
// FIXME: this test fails on rare occasions. It needs a review.
TEST (election, quorum_minimum_update_weight_before_quorum_checks)
{
nano::test::system system{};
nano::test::system system;
nano::node_config node_config{ nano::test::get_available_port (), system.logging };
node_config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled;
@ -207,8 +208,8 @@ TEST (election, quorum_minimum_update_weight_before_quorum_checks)
auto & node1 = *system.add_node (node_config);
system.wallet (0)->insert_adhoc (nano::dev::genesis_key.prv);
nano::keypair key1{};
nano::send_block_builder builder{};
nano::keypair key1;
nano::send_block_builder builder;
auto const amount = ((nano::uint256_t (node_config.online_weight_minimum.number ()) * nano::online_reps::online_weight_quorum) / 100).convert_to<nano::uint128_t> () - 1;
auto const latest = node1.latest (nano::dev::genesis_key.pub);
@ -225,7 +226,7 @@ TEST (election, quorum_minimum_update_weight_before_quorum_checks)
auto const open1 = nano::open_block_builder{}.make_block ().account (key1.pub).source (send1->hash ()).representative (key1.pub).sign (key1.prv, key1.pub).work (*system.work.generate (key1.pub)).build_shared ();
ASSERT_EQ (nano::process_result::progress, node1.process (*open1).code);
nano::keypair key2{};
nano::keypair key2;
auto const send2 = builder.make_block ()
.previous (open1->hash ())
.destination (key2.pub)
@ -242,7 +243,7 @@ TEST (election, quorum_minimum_update_weight_before_quorum_checks)
system.wallet (1)->insert_adhoc (key1.prv);
ASSERT_TIMELY (10s, node2.ledger.cache.block_count == 4);
std::shared_ptr<nano::election> election{};
std::shared_ptr<nano::election> election;
ASSERT_TIMELY (5s, (election = node1.active.election (send1->qualified_root ())) != nullptr);
ASSERT_EQ (1, election->blocks ().size ());
@ -262,7 +263,7 @@ TEST (election, quorum_minimum_update_weight_before_quorum_checks)
node1.online_reps.online_m = node_config.online_weight_minimum.number () + 20;
}
ASSERT_EQ (nano::vote_code::vote, node1.active.vote (vote2));
ASSERT_TRUE (election->confirmed ());
ASSERT_TIMELY (5s, election->confirmed ());
ASSERT_NE (nullptr, node1.block (send1->hash ()));
}
}

View file

@ -3670,15 +3670,17 @@ TEST (node, rollback_gap_source)
.sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub)
.work (*system.work.generate (nano::dev::genesis->hash ()))
.build_shared ();
auto fork = builder.make_block ()
.account (key.pub)
.previous (0)
.representative (key.pub)
.link (send1->hash ())
.balance (1)
.sign (key.prv, key.pub)
.work (*system.work.generate (key.pub))
.build_shared ();
// Side a of a forked open block receiving from send1
// This is a losing block
auto fork1a = builder.make_block ()
.account (key.pub)
.previous (0)
.representative (key.pub)
.link (send1->hash ())
.balance (1)
.sign (key.prv, key.pub)
.work (*system.work.generate (key.pub))
.build_shared ();
auto send2 = builder.make_block ()
.from (*send1)
.previous (send1->hash ())
@ -3687,63 +3689,35 @@ TEST (node, rollback_gap_source)
.sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub)
.work (*system.work.generate (send1->hash ()))
.build_shared ();
auto open = builder.make_block ()
.from (*fork)
.link (send2->hash ())
.sign (key.prv, key.pub)
.build_shared ();
// Side b of a forked open block receiving from send2.
// This is the winning block
auto fork1b = builder.make_block ()
.from (*fork1a)
.link (send2->hash ())
.sign (key.prv, key.pub)
.build_shared ();
// Set 'node' up with losing block 'fork1a'
ASSERT_EQ (nano::process_result::progress, node.process (*send1).code);
ASSERT_EQ (nano::process_result::progress, node.process (*fork).code);
// Node has fork & doesn't have source for correct block open (send2)
ASSERT_EQ (nano::process_result::progress, node.process (*fork1a).code);
// Node has 'fork1a' & doesn't have source 'send2' for winning 'fork1b' block
ASSERT_EQ (nullptr, node.block (send2->hash ()));
// Start election for fork
nano::test::start_elections (system, node, { fork });
{
auto election = node.active.election (fork->qualified_root ());
ASSERT_NE (nullptr, election);
// Process conflicting block for election
node.process_active (open);
node.block_processor.flush ();
ASSERT_EQ (2, election->blocks ().size ());
ASSERT_EQ (1, election->votes ().size ());
// Confirm open
auto vote1 (std::make_shared<nano::vote> (nano::dev::genesis_key.pub, nano::dev::genesis_key.prv, nano::vote::timestamp_max, nano::vote::duration_max, std::vector<nano::block_hash> (1, open->hash ())));
node.vote_processor.vote (vote1, std::make_shared<nano::transport::fake::channel> (node));
ASSERT_TIMELY (5s, election->votes ().size () == 2);
ASSERT_TIMELY (3s, election->confirmed ());
}
node.block_processor.force (fork1b);
ASSERT_TIMELY (5s, node.block (fork1a->hash ()) == nullptr);
// Wait for the rollback (attempt to replace fork with open)
ASSERT_TIMELY (5s, node.stats.count (nano::stat::type::rollback, nano::stat::detail::open) == 1);
ASSERT_TIMELY (5s, node.active.empty ());
// But replacing is not possible (missing source block - send2)
node.block_processor.flush ();
ASSERT_EQ (nullptr, node.block (open->hash ()));
ASSERT_EQ (nullptr, node.block (fork->hash ()));
// Fork can be returned by some other forked node or attacker
node.process_active (fork);
node.block_processor.flush ();
ASSERT_NE (nullptr, node.block (fork->hash ()));
ASSERT_EQ (nullptr, node.block (fork1b->hash ()));
// Fork can be returned by some other forked node
node.process_active (fork1a);
ASSERT_TIMELY (5s, node.block (fork1a->hash ()) != nullptr);
// With send2 block in ledger election can start again to remove fork block
ASSERT_EQ (nano::process_result::progress, node.process (*send2).code);
nano::test::start_elections (system, node, { fork });
{
auto election = node.active.election (fork->qualified_root ());
ASSERT_NE (nullptr, election);
// Process conflicting block for election
node.process_active (open);
node.block_processor.flush ();
ASSERT_EQ (2, election->blocks ().size ());
// Confirm open (again)
auto vote1 (std::make_shared<nano::vote> (nano::dev::genesis_key.pub, nano::dev::genesis_key.prv, nano::vote::timestamp_max, nano::vote::duration_max, std::vector<nano::block_hash> (1, open->hash ())));
node.vote_processor.vote (vote1, std::make_shared<nano::transport::fake::channel> (node));
ASSERT_TIMELY (5s, election->votes ().size () == 2);
}
node.block_processor.force (fork1b);
// Wait for new rollback
ASSERT_TIMELY (5s, node.stats.count (nano::stat::type::rollback, nano::stat::detail::open) == 2);
// Now fork block should be replaced with open
node.block_processor.flush ();
ASSERT_NE (nullptr, node.block (open->hash ()));
ASSERT_EQ (nullptr, node.block (fork->hash ()));
ASSERT_TIMELY (5s, node.block (fork1b->hash ()) != nullptr);
ASSERT_EQ (nullptr, node.block (fork1a->hash ()));
}
// Confirm a complex dependency graph starting from the first block

View file

@ -1,6 +1,7 @@
#include <nano/lib/jsonconfig.hpp>
#include <nano/node/transport/inproc.hpp>
#include <nano/node/vote_processor.hpp>
#include <nano/test_common/chains.hpp>
#include <nano/test_common/system.hpp>
#include <nano/test_common/testutil.hpp>
@ -61,13 +62,14 @@ TEST (vote_processor, invalid_signature)
{
nano::test::system system{ 1 };
auto & node = *system.nodes[0];
auto chain = nano::test::setup_chain (system, node, 1, nano::dev::genesis_key, false);
nano::keypair key;
auto vote = std::make_shared<nano::vote> (key.pub, key.prv, nano::vote::timestamp_min * 1, 0, std::vector<nano::block_hash>{ nano::dev::genesis->hash () });
auto vote = std::make_shared<nano::vote> (key.pub, key.prv, nano::vote::timestamp_min * 1, 0, std::vector<nano::block_hash>{ chain[0]->hash () });
auto vote_invalid = std::make_shared<nano::vote> (*vote);
vote_invalid->signature.bytes[0] ^= 1;
auto channel = std::make_shared<nano::transport::inproc::channel> (node, node);
auto election = nano::test::start_election (system, node, nano::dev::genesis->hash ());
auto election = nano::test::start_election (system, node, chain[0]->hash ());
ASSERT_NE (election, nullptr);
ASSERT_EQ (1, election->votes ().size ());

View file

@ -249,7 +249,7 @@ void nano::active_transactions::request_confirm (nano::unique_lock<nano::mutex>
bool const confirmed_l (election_l->confirmed ());
unconfirmed_count_l += !confirmed_l;
if (election_l->transition_time (solicitor))
if (confirmed_l || election_l->transition_time (solicitor))
{
erase (election_l->qualified_root);
}
@ -620,7 +620,8 @@ boost::optional<nano::election_status_type> nano::active_transactions::confirm_b
nano::unique_lock<nano::mutex> election_lock{ existing->second->mutex };
if (existing->second->status.winner && existing->second->status.winner->hash () == hash)
{
if (!existing->second->confirmed ())
// Determine if the block was confirmed explicitly via election confirmation or implicitly via confirmation height
if (!existing->second->status_confirmed ())
{
existing->second->confirm_once (election_lock, nano::election_status_type::active_confirmation_height);
status_type = nano::election_status_type::active_confirmation_height;

View file

@ -157,6 +157,11 @@ void nano::election::transition_active ()
}
bool nano::election::confirmed () const
{
return node.block_confirmed (status.winner->hash ());
}
bool nano::election::status_confirmed () const
{
return state_m == nano::election::state_t::confirmed || state_m == nano::election::state_t::expired_confirmed;
}

View file

@ -116,6 +116,12 @@ public: // State transitions
void transition_active ();
public: // Status
// Returns true when the election is confirmed in memory
// Elections will first confirm in memory once sufficient votes have been received
bool status_confirmed () const;
// Returns true when the winning block is durably confirmed in the ledger.
// Later once the confirmation height processor has updated the confirmation height it will be confirmed on disk
// It is possible for an election to be confirmed on disk but not in memory, for instance if implicitly confirmed via confirmation height
bool confirmed () const;
bool failed () const;
nano::election_extended_status current_status () const;

View file

@ -6829,6 +6829,7 @@ TEST (rpc, confirmation_active)
auto election (node1->active.election (send1->qualified_root ()));
ASSERT_NE (nullptr, election);
election->force_confirm ();
ASSERT_TIMELY (5s, election->confirmed ());
boost::property_tree::ptree request;
request.put ("action", "confirmation_active");