diff --git a/nano/core_test/active_transactions.cpp b/nano/core_test/active_transactions.cpp index dadcf36f..061eadd5 100644 --- a/nano/core_test/active_transactions.cpp +++ b/nano/core_test/active_transactions.cpp @@ -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 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) diff --git a/nano/core_test/confirmation_height.cpp b/nano/core_test/confirmation_height.cpp index ff2698d5..428eaddd 100644 --- a/nano/core_test/confirmation_height.cpp +++ b/nano/core_test/confirmation_height.cpp @@ -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 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); diff --git a/nano/core_test/election.cpp b/nano/core_test/election.cpp index 9c8e6819..fdd27a55 100644 --- a/nano/core_test/election.cpp +++ b/nano/core_test/election.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -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 () - 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 election{}; + std::shared_ptr 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 ())); } } diff --git a/nano/core_test/node.cpp b/nano/core_test/node.cpp index dd0606c6..c3547750 100644 --- a/nano/core_test/node.cpp +++ b/nano/core_test/node.cpp @@ -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::dev::genesis_key.pub, nano::dev::genesis_key.prv, nano::vote::timestamp_max, nano::vote::duration_max, std::vector (1, open->hash ()))); - node.vote_processor.vote (vote1, std::make_shared (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::dev::genesis_key.pub, nano::dev::genesis_key.prv, nano::vote::timestamp_max, nano::vote::duration_max, std::vector (1, open->hash ()))); - node.vote_processor.vote (vote1, std::make_shared (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 diff --git a/nano/core_test/vote_processor.cpp b/nano/core_test/vote_processor.cpp index 2a2fdfaa..695643b6 100644 --- a/nano/core_test/vote_processor.cpp +++ b/nano/core_test/vote_processor.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -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 (key.pub, key.prv, nano::vote::timestamp_min * 1, 0, std::vector{ nano::dev::genesis->hash () }); + auto vote = std::make_shared (key.pub, key.prv, nano::vote::timestamp_min * 1, 0, std::vector{ chain[0]->hash () }); auto vote_invalid = std::make_shared (*vote); vote_invalid->signature.bytes[0] ^= 1; auto channel = std::make_shared (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 ()); diff --git a/nano/node/active_transactions.cpp b/nano/node/active_transactions.cpp index b9f0f04d..7d0cd40b 100644 --- a/nano/node/active_transactions.cpp +++ b/nano/node/active_transactions.cpp @@ -249,7 +249,7 @@ void nano::active_transactions::request_confirm (nano::unique_lock 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::active_transactions::confirm_b nano::unique_lock 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; diff --git a/nano/node/election.cpp b/nano/node/election.cpp index 22c3f573..88dc72e9 100644 --- a/nano/node/election.cpp +++ b/nano/node/election.cpp @@ -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; } diff --git a/nano/node/election.hpp b/nano/node/election.hpp index 2db100db..dd16461c 100644 --- a/nano/node/election.hpp +++ b/nano/node/election.hpp @@ -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; diff --git a/nano/rpc_test/rpc.cpp b/nano/rpc_test/rpc.cpp index 5009a10b..8a091f7d 100644 --- a/nano/rpc_test/rpc.cpp +++ b/nano/rpc_test/rpc.cpp @@ -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");