Pulse: Verify incoming pulse blocks

This commit is contained in:
Doyle 2020-06-17 14:51:05 +10:00
parent b80bf5cbe9
commit a1c658d814
3 changed files with 102 additions and 9 deletions

View File

@ -416,16 +416,30 @@ namespace cryptonote
/* */
/************************************************************************/
struct pulse_random_value { unsigned char data[16]; };
struct pulse_verification
{
uint8_t quorum_index;
crypto::signature signature;
BEGIN_SERIALIZE()
FIELD(quorum_index);
FIELD(signature);
END_SERIALIZE();
};
struct pulse_header
{
pulse_random_value random_value;
uint8_t round;
uint16_t validator_participation_bits;
pulse_random_value random_value;
uint8_t round;
uint16_t validator_participation_bits;
std::vector<pulse_verification> verification;
BEGIN_SERIALIZE()
FIELD(random_value);
FIELD(round);
FIELD(validator_participation_bits);
FIELD(verification);
END_SERIALIZE();
};
@ -436,7 +450,7 @@ namespace cryptonote
uint64_t timestamp;
crypto::hash prev_id;
uint32_t nonce;
pulse_header pulse;
pulse_header pulse = {};
BEGIN_SERIALIZE()
VARINT_FIELD(major_version)

View File

@ -1235,6 +1235,38 @@ namespace service_nodes
return false;
}
static std::string dump_pulse_block_data(cryptonote::block const &block, service_nodes::quorum const *quorum)
{
std::stringstream stream;
std::bitset<8 * sizeof(block.pulse.validator_participation_bits)> const participation_bits = block.pulse.validator_participation_bits;
stream << "Block(" << cryptonote::get_block_height(block) << "): " << cryptonote::get_block_hash(block) << "\n";
stream << "Leader: ";
if (quorum) stream << (quorum->workers.empty() ? "(invalid leader)" : epee::string_tools::pod_to_hex(quorum->workers[0])) << "\n";
else stream << "(invalid quorum)\n";
stream << "Round: " << static_cast<int>(block.pulse.round) << "\n";
stream << "Participating Validators: " << participation_bits << "\n";
stream << "Signatures: ";
if (block.pulse.verification.empty()) stream << "(none)";
stream << "\n";
for (size_t i = 0; i < block.pulse.verification.size(); i++)
{
if (i) stream << "\n";
cryptonote::pulse_verification const &entry = block.pulse.verification[i];
stream << " [" << entry.quorum_index << "] validator: ";
if (quorum)
{
stream << (entry.quorum_index >= quorum->validators.size()) ? "(invalid quorum index)" : epee::string_tools::pod_to_hex(quorum->validators[entry.quorum_index]);
stream << ", signature: " << epee::string_tools::pod_to_hex(entry.signature);
}
else stream << "(invalid quorum)";
}
std::string result = stream.str();
return result;
}
bool service_node_list::block_added(const cryptonote::block& block, const std::vector<cryptonote::transaction>& txs, cryptonote::checkpoint_t const *checkpoint)
{
if (block.major_version < cryptonote::network_version_9_service_nodes)
@ -1243,6 +1275,52 @@ namespace service_nodes
std::lock_guard lock(m_sn_mutex);
process_block(block, txs);
if (block.major_version >= cryptonote::network_version_16)
{
assert(m_state.block_hash == cryptonote::get_block_hash(block));
std::shared_ptr<const quorum> quorum = get_quorum(quorum_type::pulse, cryptonote::get_block_height(block));
if (quorum)
{
if (block.pulse.verification.size() != PULSE_BLOCK_REQUIRED_SIGNATURES)
{
LOG_PRINT_L1("Pulse block(" << m_state.height << "): " << m_state.block_hash << " has " << block.pulse.verification.size() << " signatures but requires " << PULSE_BLOCK_REQUIRED_SIGNATURES << "\n" << dump_pulse_block_data(block, quorum.get()));
return false;
}
for (cryptonote::pulse_verification const &verification : block.pulse.verification)
{
if (verification.quorum_index >= quorum->validators.size())
return false;
uint16_t mask = 1 << verification.quorum_index;
if ((block.pulse.validator_participation_bits & mask) == 0)
{
LOG_PRINT_L1("Received pulse signature from validator " << verification.quorum_index << " that is not participating in round " << block.pulse.round << "\n" << dump_pulse_block_data(block, quorum.get()));
return false;
}
auto buf_to_hash = tools::memcpy_le(m_state.block_hash.data, block.pulse.random_value.data, block.pulse.round, block.pulse.validator_participation_bits);
crypto::hash prefix_hash = crypto::cn_fast_hash(buf_to_hash.data(), buf_to_hash.size());
if (!crypto::check_signature(prefix_hash, quorum->validators[verification.quorum_index], verification.signature))
{
LOG_PRINT_L1("Received pulse signature from validator " << verification.quorum_index << " that is invalid in round " << block.pulse.round << "\n" << dump_pulse_block_data(block, quorum.get()));
return false;
}
}
}
else
{
bool expect_pulse_quorum = (block.pulse.validator_participation_bits > 0 || block.pulse.verification.size());
if (expect_pulse_quorum)
{
LOG_PRINT_L1("Failed to get pulse quorum for block\n" << dump_pulse_block_data(block, quorum.get()));
return false;
}
// NOTE: Otherwise, block has no pulse information, there's no expected
// quorum, this is a fallback- nonce mined block, previously verified validly
}
}
if (block.major_version >= cryptonote::network_version_13_enforce_checkpoints && checkpoint)
{
std::shared_ptr<const quorum> quorum = get_quorum(quorum_type::checkpointing, checkpoint->height);
@ -1506,14 +1584,13 @@ namespace service_nodes
{
if (active_snode_list.size() < (PULSE_QUORUM_SIZE + 1 /*leader*/))
{
// TODO(doyle): We need fallback code
MERROR("Insufficient active Service Nodes for Pulse: " << active_snode_list.size());
LOG_PRINT_L2("Insufficient active Service Nodes for Pulse: " << active_snode_list.size() << ", on block(" << state.height << "): " << state.block_hash);
continue;
}
if (pulse_entropy.size() < PULSE_QUORUM_SIZE)
{
MERROR("Blockchain has insufficient blocks to generate Pulse data");
LOG_PRINT_L2("Blockchain has insufficient blocks to generate Pulse data on block(" << state.height << "): " << state.block_hash);
continue;
}

View File

@ -5,8 +5,10 @@
#include "service_node_voting.h"
namespace service_nodes {
constexpr size_t PULSE_QUORUM_ENTROPY_LAG = 21; // How many blocks back from the tip of the Blockchain to source entropy for the Pulse quorums.
constexpr size_t PULSE_QUORUM_SIZE = 11;
constexpr size_t PULSE_QUORUM_ENTROPY_LAG = 21; // How many blocks back from the tip of the Blockchain to source entropy for the Pulse quorums.
constexpr size_t PULSE_QUORUM_SIZE = 11;
constexpr size_t PULSE_BLOCK_REQUIRED_SIGNATURES = 7; // A block must have exactly N signatures to be considered properly
static_assert(PULSE_QUORUM_SIZE >= PULSE_BLOCK_REQUIRED_SIGNATURES, "Quorum must be larger than the required number of signatures.");
static_assert(PULSE_QUORUM_ENTROPY_LAG >= PULSE_QUORUM_SIZE, "We need to pull atleast PULSE_QUORUM_SIZE number of blocks from the Blockchain, we can't if the amount of blocks to go back from the tip of the Blockchain is less than the blocks we need.");
// Service node decommissioning: as service nodes stay up they earn "credits" (measured in blocks)