Fix vote republish rules and duplicate sequence number detection

This commit is contained in:
clemahieu 2018-03-15 14:16:16 -05:00
commit 63e15c21ae
7 changed files with 205 additions and 81 deletions

View file

@ -1262,22 +1262,6 @@ std::shared_ptr<rai::vote> rai::block_store::vote_max (MDB_txn * transaction_a,
return result;
}
rai::vote_result rai::block_store::vote_validate (MDB_txn * transaction_a, std::shared_ptr<rai::vote> vote_a)
{
rai::vote_result result ({ rai::vote_code::invalid, 0 });
// Reject unsigned votes
if (!rai::validate_message (vote_a->account, vote_a->hash (), vote_a->signature))
{
result.code = rai::vote_code::replay;
result.vote = vote_max (transaction_a, vote_a); // Make sure this sequence number is > any we've seen from this account before
if (result.vote == vote_a)
{
result.code = rai::vote_code::vote;
}
}
return result;
}
rai::store_iterator rai::block_store::latest_begin (MDB_txn * transaction_a, rai::account const & account_a)
{
rai::store_iterator result (transaction_a, accounts, rai::mdb_val (account_a));

View file

@ -119,7 +119,6 @@ public:
bool checksum_get (MDB_txn *, uint64_t, uint8_t, rai::checksum &);
void checksum_del (MDB_txn *, uint64_t, uint8_t);
rai::vote_result vote_validate (MDB_txn *, std::shared_ptr<rai::vote>);
// Return latest vote for an account from store
std::shared_ptr<rai::vote> vote_get (MDB_txn *, rai::account const &);
// Populate vote with the next sequence number

View file

@ -755,29 +755,6 @@ TEST (block_store, block_random)
ASSERT_EQ (*block, *genesis.open);
}
TEST (vote, validate)
{
bool init (false);
rai::block_store store (init, rai::unique_path ());
ASSERT_TRUE (!init);
rai::keypair key1;
auto send1 (std::make_shared<rai::send_block> (0, key1.pub, 0, rai::test_genesis_key.prv, rai::test_genesis_key.pub, 0));
auto vote1 (std::make_shared<rai::vote> (key1.pub, key1.prv, 2, send1));
rai::transaction transaction (store.environment, nullptr, true);
auto vote_result1 (store.vote_validate (transaction, vote1));
ASSERT_EQ (rai::vote_code::vote, vote_result1.code);
ASSERT_EQ (*vote1, *vote_result1.vote);
vote1->signature.bytes[8] ^= 1;
auto vote_result2 (store.vote_validate (transaction, vote1));
ASSERT_EQ (rai::vote_code::invalid, vote_result2.code);
// If the signature is invalid, we don't need to take the overhead of checking the current sequence value
ASSERT_EQ (nullptr, vote_result2.vote);
auto vote2 (std::make_shared<rai::vote> (key1.pub, key1.prv, 1, send1));
auto vote_result3 (store.vote_validate (transaction, vote2));
ASSERT_EQ (rai::vote_code::replay, vote_result3.code);
ASSERT_EQ (*vote1, *vote_result3.vote);
}
TEST (block_store, upgrade_v5_v6)
{
auto path (rai::unique_path ());

View file

@ -731,6 +731,32 @@ TEST (ledegr, double_receive)
ASSERT_EQ (rai::process_result::unreceivable, ledger.process (transaction, receive1).code);
}
TEST (votes, check_signature)
{
rai::system system (24000, 1);
auto & node1 (*system.nodes[0]);
rai::genesis genesis;
rai::keypair key1;
auto send1 (std::make_shared<rai::send_block> (genesis.hash (), key1.pub, rai::genesis_amount - 100, rai::test_genesis_key.prv, rai::test_genesis_key.pub, 0));
{
rai::transaction transaction (node1.store.environment, nullptr, true);
ASSERT_EQ (rai::process_result::progress, node1.ledger.process (transaction, *send1).code);
}
auto node_l (system.nodes[0]);
{
rai::transaction transaction (node1.store.environment, nullptr, true);
node1.active.start (transaction, send1);
}
auto votes1 (node1.active.roots.find (send1->root ())->election);
ASSERT_EQ (1, votes1->votes.rep_votes.size ());
auto vote1 (std::make_shared<rai::vote> (rai::test_genesis_key.pub, rai::test_genesis_key.prv, 1, send1));
vote1->signature.bytes [0] ^= 1;
ASSERT_EQ (rai::vote_code::invalid, node1.vote_processor.vote (vote1, rai::endpoint ()).code);
vote1->signature.bytes [0] ^= 1;
ASSERT_EQ (rai::vote_code::vote, node1.vote_processor.vote (vote1, rai::endpoint ()).code);
ASSERT_EQ (rai::vote_code::replay, node1.vote_processor.vote (vote1, rai::endpoint ()).code);
}
TEST (votes, add_one)
{
rai::system system (24000, 1);
@ -819,7 +845,12 @@ TEST (votes, add_existing)
rai::keypair key2;
auto send2 (std::make_shared<rai::send_block> (genesis.hash (), key2.pub, 0, rai::test_genesis_key.prv, rai::test_genesis_key.pub, 0));
auto vote2 (std::make_shared<rai::vote> (rai::test_genesis_key.pub, rai::test_genesis_key.prv, 2, send2));
// Pretend we've waited the timeout
votes1->last_votes[rai::test_genesis_key.pub].first = std::chrono::steady_clock::now () - std::chrono::seconds (20);
votes1->vote (vote2);
// Also resend the old vote, and see if we respect the sequence number
votes1->last_votes[rai::test_genesis_key.pub].first = std::chrono::steady_clock::now () - std::chrono::seconds (20);
votes1->vote (vote1);
ASSERT_EQ (2, votes1->votes.rep_votes.size ());
ASSERT_NE (votes1->votes.rep_votes.end (), votes1->votes.rep_votes.find (rai::test_genesis_key.pub));
ASSERT_EQ (*send2, *votes1->votes.rep_votes[rai::test_genesis_key.pub]);
@ -851,6 +882,86 @@ TEST (votes, add_old)
rai::keypair key2;
auto send2 (std::make_shared<rai::send_block> (genesis.hash (), key2.pub, 0, rai::test_genesis_key.prv, rai::test_genesis_key.pub, 0));
auto vote2 (std::make_shared<rai::vote> (rai::test_genesis_key.pub, rai::test_genesis_key.prv, 1, send2));
votes1->last_votes[rai::test_genesis_key.pub].first = std::chrono::steady_clock::now () - std::chrono::seconds (20);
node1.vote_processor.vote (vote2, rai::endpoint ());
ASSERT_EQ (2, votes1->votes.rep_votes.size ());
ASSERT_NE (votes1->votes.rep_votes.end (), votes1->votes.rep_votes.find (rai::test_genesis_key.pub));
ASSERT_EQ (*send1, *votes1->votes.rep_votes[rai::test_genesis_key.pub]);
rai::transaction transaction (system.nodes[0]->store.environment, nullptr, false);
auto winner (node1.ledger.winner (transaction, votes1->votes));
ASSERT_EQ (*send1, *winner.second);
}
// Lower sequence numbers are accepted for different accounts
TEST (votes, add_old_different_account)
{
rai::system system (24000, 1);
auto & node1 (*system.nodes[0]);
rai::genesis genesis;
rai::keypair key1;
auto send1 (std::make_shared<rai::send_block> (genesis.hash (), key1.pub, 0, rai::test_genesis_key.prv, rai::test_genesis_key.pub, 0));
auto send2 (std::make_shared<rai::send_block> (send1->hash (), key1.pub, 0, rai::test_genesis_key.prv, rai::test_genesis_key.pub, 0));
{
rai::transaction transaction (node1.store.environment, nullptr, true);
ASSERT_EQ (rai::process_result::progress, node1.ledger.process (transaction, *send1).code);
ASSERT_EQ (rai::process_result::progress, node1.ledger.process (transaction, *send2).code);
}
auto node_l (system.nodes[0]);
{
rai::transaction transaction (node1.store.environment, nullptr, true);
node1.active.start (transaction, send1);
node1.active.start (transaction, send2);
}
auto votes1 (node1.active.roots.find (send1->root ())->election);
auto votes2 (node1.active.roots.find (send2->root ())->election);
ASSERT_EQ (1, votes1->votes.rep_votes.size ());
ASSERT_EQ (1, votes2->votes.rep_votes.size ());
auto vote1 (std::make_shared<rai::vote> (rai::test_genesis_key.pub, rai::test_genesis_key.prv, 2, send1));
auto vote_result1 (node1.vote_processor.vote (vote1, rai::endpoint ()));
ASSERT_EQ (rai::vote_code::vote, vote_result1.code);
ASSERT_EQ (*vote1, *vote_result1.vote);
ASSERT_EQ (2, votes1->votes.rep_votes.size ());
ASSERT_EQ (1, votes2->votes.rep_votes.size ());
auto vote2 (std::make_shared<rai::vote> (rai::test_genesis_key.pub, rai::test_genesis_key.prv, 1, send2));
auto vote_result2 (node1.vote_processor.vote (vote2, rai::endpoint ()));
ASSERT_EQ (rai::vote_code::vote, vote_result2.code);
ASSERT_EQ (*vote2, *vote_result2.vote);
ASSERT_EQ (2, votes1->votes.rep_votes.size ());
ASSERT_EQ (2, votes2->votes.rep_votes.size ());
ASSERT_NE (votes1->votes.rep_votes.end (), votes1->votes.rep_votes.find (rai::test_genesis_key.pub));
ASSERT_NE (votes2->votes.rep_votes.end (), votes2->votes.rep_votes.find (rai::test_genesis_key.pub));
ASSERT_EQ (*send1, *votes1->votes.rep_votes[rai::test_genesis_key.pub]);
ASSERT_EQ (*send2, *votes2->votes.rep_votes[rai::test_genesis_key.pub]);
rai::transaction transaction (system.nodes[0]->store.environment, nullptr, false);
auto winner1 (node1.ledger.winner (transaction, votes1->votes));
ASSERT_EQ (*send1, *winner1.second);
auto winner2 (node1.ledger.winner (transaction, votes2->votes));
ASSERT_EQ (*send2, *winner2.second);
}
// The voting cooldown is respected
TEST (votes, add_cooldown)
{
rai::system system (24000, 1);
auto & node1 (*system.nodes[0]);
rai::genesis genesis;
rai::keypair key1;
auto send1 (std::make_shared<rai::send_block> (genesis.hash (), key1.pub, 0, rai::test_genesis_key.prv, rai::test_genesis_key.pub, 0));
{
rai::transaction transaction (node1.store.environment, nullptr, true);
ASSERT_EQ (rai::process_result::progress, node1.ledger.process (transaction, *send1).code);
}
auto node_l (system.nodes[0]);
{
rai::transaction transaction (node1.store.environment, nullptr, true);
node1.active.start (transaction, send1);
}
auto votes1 (node1.active.roots.find (send1->root ())->election);
auto vote1 (std::make_shared<rai::vote> (rai::test_genesis_key.pub, rai::test_genesis_key.prv, 1, send1));
node1.vote_processor.vote (vote1, rai::endpoint ());
rai::keypair key2;
auto send2 (std::make_shared<rai::send_block> (genesis.hash (), key2.pub, 0, rai::test_genesis_key.prv, rai::test_genesis_key.pub, 0));
auto vote2 (std::make_shared<rai::vote> (rai::test_genesis_key.pub, rai::test_genesis_key.prv, 2, send2));
node1.vote_processor.vote (vote2, rai::endpoint ());
ASSERT_EQ (2, votes1->votes.rep_votes.size ());
ASSERT_NE (votes1->votes.rep_votes.end (), votes1->votes.rep_votes.find (rai::test_genesis_key.pub));

View file

@ -222,26 +222,23 @@ void rai::network::republish_block (MDB_txn * transaction, std::shared_ptr<rai::
// In order to rate limit network traffic we republish:
// 1) Only if they are a non-replay vote of a block that's actively settling. Settling blocks are limited by block PoW
// 2) Only if a vote for this block hasn't been received in the previous X second. This prevents rapid publishing of votes with increasing sequence numbers.
// 3) The rep has a weight > Y to prevent creating a lot of small-weight accounts to send out votes
void rai::network::republish_vote (std::chrono::steady_clock::time_point const & last_vote, std::shared_ptr<rai::vote> vote_a)
// 2) The rep has a weight > Y to prevent creating a lot of small-weight accounts to send out votes
// 3) Only if a vote for this block from this representative hasn't been received in the previous X second.
// This prevents rapid publishing of votes with increasing sequence numbers.
//
// These rules are implemented by the caller, not this function.
void rai::network::republish_vote (std::shared_ptr<rai::vote> vote_a)
{
if (last_vote < std::chrono::steady_clock::now () - std::chrono::seconds (1))
rai::confirm_ack confirm (vote_a);
std::shared_ptr<std::vector<uint8_t>> bytes (new std::vector<uint8_t>);
{
if (node.weight (vote_a->account) > rai::Mxrb_ratio * 256)
{
rai::confirm_ack confirm (vote_a);
std::shared_ptr<std::vector<uint8_t>> bytes (new std::vector<uint8_t>);
{
rai::vectorstream stream (*bytes);
confirm.serialize (stream);
}
auto list (node.peers.list_sqrt ());
for (auto j (list.begin ()), m (list.end ()); j != m; ++j)
{
node.network.confirm_send (confirm, bytes, *j);
}
}
rai::vectorstream stream (*bytes);
confirm.serialize (stream);
}
auto list (node.peers.list_sqrt ());
for (auto j (list.begin ()), m (list.end ()); j != m; ++j)
{
node.network.confirm_send (confirm, bytes, *j);
}
}
@ -373,11 +370,10 @@ public:
auto vote (node.vote_processor.vote (message_a.vote, sender));
if (vote.code == rai::vote_code::replay)
{
assert (vote.vote->sequence > message_a.vote->sequence);
// This tries to assist rep nodes that have lost track of their highest sequence number by replaying our highest known vote back to them
// Only do this if the sequence number is significantly different to account for network reordering
// Amplify attack considerations: We're sending out a confirm_ack in response to a confirm_ack for no net traffic increase
if (vote.vote->sequence - message_a.vote->sequence > 10000)
if (vote.vote->sequence > message_a.vote->sequence + 10000)
{
rai::confirm_ack confirm (vote.vote);
std::shared_ptr<std::vector<uint8_t>> bytes (new std::vector<uint8_t>);
@ -1084,10 +1080,23 @@ node (node_a)
rai::vote_result rai::vote_processor::vote (std::shared_ptr<rai::vote> vote_a, rai::endpoint endpoint_a)
{
rai::vote_result result;
rai::vote_result result = { rai::vote_code::invalid, vote_a };
if (!rai::validate_message (vote_a->account, vote_a->hash (), vote_a->signature))
{
rai::transaction transaction (node.store.environment, nullptr, false);
result = node.store.vote_validate (transaction, vote_a);
result.code = rai::vote_code::replay;
std::shared_ptr<rai::vote> newest_vote;
{
rai::transaction transaction (node.store.environment, nullptr, false);
newest_vote = node.store.vote_max (transaction, vote_a);
}
if (!node.active.vote (vote_a))
{
result.code = rai::vote_code::vote;
}
else
{
result.vote = newest_vote;
}
}
if (node.config.logging.vote_logging ())
{
@ -1557,9 +1566,6 @@ block_processor_thread ([this]() { this->block_processor.process_blocks (); })
this->network.send_keepalive (endpoint_a);
rep_query (*this, endpoint_a);
});
observers.vote.add ([this](std::shared_ptr<rai::vote> vote_a, rai::endpoint const &) {
active.vote (vote_a);
});
observers.vote.add ([this](std::shared_ptr<rai::vote> vote_a, rai::endpoint const &) {
this->gap_cache.vote (vote_a);
});
@ -2791,7 +2797,6 @@ rai::election::election (MDB_txn * transaction_a, rai::node & node_a, std::share
confirmation_action (confirmation_action_a),
votes (block_a),
node (node_a),
last_vote (std::chrono::steady_clock::now ()),
last_winner (block_a)
{
assert (node_a.store.block_exists (transaction_a, block_a->hash ()));
@ -2893,14 +2898,59 @@ void rai::election::confirm_cutoff (MDB_txn * transaction_a)
confirm_once (transaction_a);
}
void rai::election::vote (std::shared_ptr<rai::vote> vote_a)
bool rai::election::vote (std::shared_ptr<rai::vote> vote_a)
{
node.network.republish_vote (last_vote, vote_a);
last_vote = std::chrono::steady_clock::now ();
assert (!rai::validate_message (vote_a->account, vote_a->hash (), vote_a->signature));
// see republish_vote documentation for an explanation of these rules
rai::transaction transaction (node.store.environment, nullptr, true);
assert (node.store.vote_validate (transaction, vote_a).code != rai::vote_code::invalid);
votes.vote (vote_a);
confirm_if_quorum (transaction);
auto replay (false);
auto supply (node.ledger.supply (transaction));
auto weight (node.ledger.weight (transaction, vote_a->account));
if (rai::rai_network == rai::rai_networks::rai_test_network || weight > supply / 1000) // 0.1% or above
{
unsigned int cooldown;
if (weight < supply / 100) // 0.1% to 1%
{
cooldown = 15;
}
else if (weight < supply / 20) // 1% to 5%
{
cooldown = 5;
}
else // 5% or above
{
cooldown = 1;
}
auto should_process (false);
auto last_vote_it (last_votes.find (vote_a->account));
if (last_vote_it == last_votes.end ())
{
should_process = true;
}
else
{
auto last_vote (last_vote_it->second);
if (vote_a->sequence > last_vote.second)
{
if (last_vote.first <= std::chrono::steady_clock::now () - std::chrono::seconds (cooldown))
{
should_process = true;
}
}
else
{
replay = true;
}
}
if (should_process)
{
last_votes.insert (std::make_pair (vote_a->account, std::make_pair (std::chrono::steady_clock::now (), vote_a->sequence)));
node.network.republish_vote (vote_a);
votes.vote (vote_a);
confirm_if_quorum (transaction);
}
}
return replay;
}
void rai::active_transactions::announce_votes ()
@ -2979,7 +3029,7 @@ bool rai::active_transactions::start (MDB_txn * transaction_a, std::shared_ptr<r
}
// Validate a vote and apply it to the current election if one exists
void rai::active_transactions::vote (std::shared_ptr<rai::vote> vote_a)
bool rai::active_transactions::vote (std::shared_ptr<rai::vote> vote_a)
{
std::shared_ptr<rai::election> election;
{
@ -2991,10 +3041,12 @@ void rai::active_transactions::vote (std::shared_ptr<rai::vote> vote_a)
election = existing->election;
}
}
auto result (false);
if (election)
{
election->vote (vote_a);
result = election->vote (vote_a);
}
return result;
}
bool rai::active_transactions::active (rai::block const & block_a)

View file

@ -43,7 +43,7 @@ class election : public std::enable_shared_from_this<rai::election>
public:
election (MDB_txn *, rai::node &, std::shared_ptr<rai::block>, std::function<void(std::shared_ptr<rai::block>, bool)> const &);
void vote (std::shared_ptr<rai::vote>);
bool vote (std::shared_ptr<rai::vote>);
// Check if we have vote quorum
bool have_quorum (MDB_txn *);
// Tell the network our view of the winner
@ -58,7 +58,7 @@ public:
rai::uint128_t minimum_threshold (MDB_txn *, rai::ledger &);
rai::votes votes;
rai::node & node;
std::chrono::steady_clock::time_point last_vote;
std::unordered_map<rai::account, std::pair<std::chrono::steady_clock::time_point, uint64_t>> last_votes;
std::shared_ptr<rai::block> last_winner;
std::atomic_flag confirmed;
};
@ -79,7 +79,9 @@ public:
// Start an election for a block
// Call action with confirmed block, may be different than what we started with
bool start (MDB_txn *, std::shared_ptr<rai::block>, std::function<void(std::shared_ptr<rai::block>, bool)> const & = [](std::shared_ptr<rai::block>, bool) {});
void vote (std::shared_ptr<rai::vote>);
// If this returns true, the vote is a replay
// If this returns false, the vote may or may not be a replay
bool vote (std::shared_ptr<rai::vote>);
// Is the root of this block in the roots container
bool active (rai::block const &);
void announce_votes ();
@ -304,7 +306,7 @@ public:
void receive_action (boost::system::error_code const &, size_t);
void rpc_action (boost::system::error_code const &, size_t);
void rebroadcast_reps (std::shared_ptr<rai::block>);
void republish_vote (std::chrono::steady_clock::time_point const &, std::shared_ptr<rai::vote>);
void republish_vote (std::shared_ptr<rai::vote>);
void republish_block (MDB_txn *, std::shared_ptr<rai::block>);
void republish (rai::block_hash const &, std::shared_ptr<std::vector<uint8_t>>, rai::endpoint);
void publish_broadcast (std::vector<rai::peer_information> &, std::unique_ptr<rai::block>);

View file

@ -424,8 +424,7 @@ TEST (store, vote_load)
auto block (std::make_shared<rai::send_block> (0, 0, 0, rai::test_genesis_key.prv, rai::test_genesis_key.pub, 0));
for (auto i (0); i < 1000000; ++i)
{
rai::transaction transaction (node.store.environment, nullptr, true);
auto vote (std::make_shared<rai::vote> (rai::test_genesis_key.pub, rai::test_genesis_key.prv, i, block));
node.store.vote_validate (transaction, vote);
node.vote_processor.vote (vote, system.nodes[0]->network.endpoint ());
}
}