From 937a2fd39dc57473fc43647faf127a960aa176e7 Mon Sep 17 00:00:00 2001 From: Doyle Date: Tue, 11 Aug 2020 18:14:17 +1000 Subject: [PATCH] Pulse: Add random value hash stage --- src/cryptonote_core/pulse.cpp | 231 ++++++++++++++++++----- src/cryptonote_core/pulse.h | 7 + src/cryptonote_core/service_node_rules.h | 21 +-- src/cryptonote_protocol/quorumnet.cpp | 92 +++++++-- 4 files changed, 271 insertions(+), 80 deletions(-) diff --git a/src/cryptonote_core/pulse.cpp b/src/cryptonote_core/pulse.cpp index 5e3674b0f..3ceec5428 100644 --- a/src/cryptonote_core/pulse.cpp +++ b/src/cryptonote_core/pulse.cpp @@ -28,6 +28,9 @@ enum struct round_state submit_block_template, wait_for_block_template, + + submit_random_value_hash, + wait_for_random_value_hashes, }; constexpr std::string_view round_state_string(round_state state) @@ -47,6 +50,9 @@ constexpr std::string_view round_state_string(round_state state) case round_state::submit_block_template: return "Submit Block Template"sv; case round_state::wait_for_block_template: return "Wait For Block Template"sv; + + case round_state::submit_random_value_hash: return "Submit Random Value Hash"sv; + case round_state::wait_for_random_value_hashes: return "Wait For Random Value Hash"sv; } return "Invalid2"sv; @@ -107,6 +113,17 @@ struct round_context pulse::time_point end_time; } wait_for_block_template; + struct + { + cryptonote::pulse_random_value value; + } submit_random_value_hash; + + struct + { + std::array, service_nodes::PULSE_QUORUM_NUM_VALIDATORS> hashes; + pulse::time_point end_time; + } wait_for_random_value_hashes; + round_state state; }; @@ -147,12 +164,49 @@ bool msg_time_check(round_context const &context, pulse::message const &msg, pul return true; } +crypto::hash make_message_signature_hash(round_context const &context, pulse::message const &msg) +{ + assert(context.state >= round_state::wait_for_next_block); + crypto::hash result = {}; + switch(msg.type) + { + case pulse::message_type::invalid: + assert("Invalid Code Path" == nullptr); + break; + + case pulse::message_type::handshake: + { + auto buf = tools::memcpy_le(context.wait_for_next_block.top_hash.data, msg.quorum_position); + result = crypto::cn_fast_hash(buf.data(), buf.size()); + } + break; + + case pulse::message_type::handshake_bitset: + { + auto buf = tools::memcpy_le(msg.handshakes.validator_bitset, context.wait_for_next_block.top_hash.data, msg.quorum_position); + result = crypto::cn_fast_hash(buf.data(), buf.size()); + } + break; + + case pulse::message_type::block_template: + result = crypto::cn_fast_hash(msg.block_template.blob.data(), msg.block_template.blob.size()); + break; + + case pulse::message_type::random_value_hash: + { + auto buf = tools::memcpy_le(context.wait_for_next_block.top_hash.data, msg.quorum_position, msg.random_value_hash.hash.data); + result = crypto::cn_fast_hash(buf.data(), buf.size()); + } + break; + } + + return result; +} + bool message_signature_check(pulse::message const &msg, service_nodes::quorum const &quorum) { - // Generate hash that was signed to a signature + // Get Service Node Key crypto::public_key const *key = nullptr; - crypto::hash hash = {}; - switch (msg.type) { case pulse::message_type::invalid: @@ -164,7 +218,8 @@ bool message_signature_check(pulse::message const &msg, service_nodes::quorum co break; case pulse::message_type::handshake: /* FALLTHRU */ - case pulse::message_type::handshake_bitset: + case pulse::message_type::handshake_bitset: /* FALLTHRU */ + case pulse::message_type::random_value_hash: { if (msg.quorum_position >= static_cast(quorum.validators.size())) { @@ -173,16 +228,6 @@ bool message_signature_check(pulse::message const &msg, service_nodes::quorum co } key = &quorum.validators[msg.quorum_position]; - if (msg.type == pulse::message_type::handshake) - { - auto buf = tools::memcpy_le(context.wait_for_next_block.top_hash.data, msg.quorum_position); - hash = crypto::cn_fast_hash(buf.data(), buf.size()); - } - else - { - auto buf = tools::memcpy_le(msg.handshakes.validator_bitset, context.wait_for_next_block.top_hash.data, msg.quorum_position); - hash = crypto::cn_fast_hash(buf.data(), buf.size()); - } } break; @@ -194,14 +239,13 @@ bool message_signature_check(pulse::message const &msg, service_nodes::quorum co return false; } - key = &context.prepare_for_round.quorum.workers[0]; - hash = crypto::cn_fast_hash(msg.block_template.blob.data(), msg.block_template.blob.size()); + key = &context.prepare_for_round.quorum.workers[0]; } break; } - // Verify hash with signature - if (!crypto::check_signature(hash, *key, msg.signature)) + // Verify + if (!crypto::check_signature(make_message_signature_hash(context, msg), *key, msg.signature)) { MERROR(log_prefix(context) << "Signature from pulse handshake bit does not validate with node " << msg.quorum_position << ":" << lokimq::to_hex(tools::view_guts(*key)) << ", at height " << context.wait_for_next_block.height << "; Node signing outdated height or bad handshake data"); return false; @@ -287,6 +331,21 @@ void pulse::handle_message(void *quorumnet_state, pulse::message const &msg) } break; + + case pulse::message_type::random_value_hash: + { + if (!msg_time_check(context, msg, pulse::clock::now(), context.wait_for_handshakes.start_time, context.wait_for_random_value_hashes.end_time)) + return; + + auto &[hash, received] = context.wait_for_random_value_hashes.hashes[msg.quorum_position]; + if (received) + return; // Already received their hash + + received = true; + relay_message = true; + hash = msg.random_value_hash.hash; + } + break; } if (relay_message) @@ -457,12 +516,20 @@ Yes +-----[Insufficient Bitsets] */ + enum struct event_loop { keep_running, return_to_caller, }; +event_loop goto_preparing_for_next_round(round_context &context) +{ + context.state = round_state::prepare_for_round; + context.prepare_for_round.queue_for_next_round = true; + return event_loop::keep_running; +} + event_loop wait_for_next_block(uint64_t hf16_height, round_context &context, cryptonote::Blockchain const &blockchain) { // @@ -540,6 +607,8 @@ event_loop prepare_for_round(round_context &context, service_nodes::service_node context.wait_for_handshake_bitsets = {}; context.submit_block_template = {}; context.wait_for_block_template = {}; + context.submit_random_value_hash = {}; + context.wait_for_random_value_hashes = {}; if (context.prepare_for_round.queue_for_next_round) { @@ -568,10 +637,11 @@ event_loop prepare_for_round(round_context &context, service_nodes::service_node } auto start_time = context.wait_for_next_block.round_0_start_time + (context.prepare_for_round.round * service_nodes::PULSE_ROUND_TIME); - context.wait_for_handshakes.start_time = start_time; - context.wait_for_handshakes.end_time = start_time + service_nodes::PULSE_WAIT_FOR_HANDSHAKES_DURATION; - context.wait_for_handshake_bitsets.end_time = context.wait_for_handshakes.end_time + service_nodes::PULSE_WAIT_FOR_OTHER_VALIDATOR_HANDSHAKES_DURATION; - context.wait_for_block_template.end_time = context.wait_for_handshake_bitsets.end_time + service_nodes::PULSE_WAIT_FOR_BLOCK_TEMPLATE_DURATION; + context.wait_for_handshakes.start_time = start_time; + context.wait_for_handshakes.end_time = start_time + service_nodes::PULSE_WAIT_FOR_HANDSHAKES_DURATION; + context.wait_for_handshake_bitsets.end_time = context.wait_for_handshakes.end_time + service_nodes::PULSE_WAIT_FOR_OTHER_VALIDATOR_HANDSHAKES_DURATION; + context.wait_for_block_template.end_time = context.wait_for_handshake_bitsets.end_time + service_nodes::PULSE_WAIT_FOR_BLOCK_TEMPLATE_DURATION; + context.wait_for_random_value_hashes.end_time = context.wait_for_block_template.end_time + service_nodes::PULSE_WAIT_FOR_RANDOM_VALUE_HASH_DURATION; context.prepare_for_round.quorum = service_nodes::generate_pulse_quorum(blockchain.nettype(), @@ -618,14 +688,10 @@ event_loop prepare_for_round(round_context &context, service_nodes::service_node if (context.prepare_for_round.participant == sn_type::none) { MINFO(log_prefix(context) << "We are not a pulse validator. Waiting for next pulse round or block."); - context.state = round_state::prepare_for_round; - context.prepare_for_round.queue_for_next_round = true; - } - else - { - context.state = round_state::wait_for_round; + return goto_preparing_for_next_round(context); } + context.state = round_state::wait_for_round; return event_loop::keep_running; } @@ -672,8 +738,7 @@ event_loop submit_handshakes(round_context &context, void *quorumnet_state) catch (std::exception const &e) { MERROR(log_prefix(context) << "Attempting to invoke and send a Pulse participation handshake unexpectedly failed. " << e.what()); - context.state = round_state::prepare_for_round; - context.prepare_for_round.queue_for_next_round = true; + return goto_preparing_for_next_round(context); } return event_loop::return_to_caller; @@ -718,8 +783,7 @@ event_loop submit_handshake_bitset(round_context &context, void *quorumnet_state catch(std::exception const &e) { MERROR(log_prefix(context) << "Attempting to invoke and send a Pulse validator bitset unexpectedly failed. " << e.what()); - context.state = round_state::prepare_for_round; - context.prepare_for_round.queue_for_next_round = true; + return goto_preparing_for_next_round(context); } return event_loop::keep_running; @@ -772,21 +836,18 @@ event_loop wait_for_handshake_bitsets(round_context &context) << max_bitsets << ", waiting for next round."); } - context.state = round_state::prepare_for_round; - context.prepare_for_round.queue_for_next_round = true; + return goto_preparing_for_next_round(context); } + + std::bitset<8 * sizeof(most_common_bitset)> bitset = most_common_bitset; + context.submit_block_template.validator_bitset = most_common_bitset; + + MINFO(log_prefix(context) << count << "/" << max_bitsets << " validators agreed on the participating nodes in the quorum " << bitset << (context.prepare_for_round.participant == sn_type::producer ? "" : ". Awaiting block template from block producer")); + + if (context.prepare_for_round.participant == sn_type::producer) + context.state = round_state::submit_block_template; else - { - std::bitset<8 * sizeof(most_common_bitset)> bitset = most_common_bitset; - context.submit_block_template.validator_bitset = most_common_bitset; - - MINFO(log_prefix(context) << count << "/" << max_bitsets << " validators agreed on the participating nodes in the quorum " << bitset << (context.prepare_for_round.participant == sn_type::producer ? "" : ". Awaiting block template from block producer")); - - if (context.prepare_for_round.participant == sn_type::producer) - context.state = round_state::submit_block_template; - else - context.state = round_state::wait_for_block_template; - } + context.state = round_state::wait_for_block_template; return event_loop::keep_running; } @@ -802,9 +863,7 @@ event_loop submit_block_template(round_context &context, service_nodes::service_ if (list_state.empty()) { MINFO(log_prefix(context) << "Block producer (us) is not available on the service node list, waiting until next round"); - context.state = round_state::prepare_for_round; - context.prepare_for_round.queue_for_next_round = true; - return event_loop::keep_running; + return goto_preparing_for_next_round(context); } // TODO(doyle): These checks can be done earlier? @@ -812,9 +871,7 @@ event_loop submit_block_template(round_context &context, service_nodes::service_ if (!info->is_active()) { MINFO(log_prefix(context) << "Block producer (us) is not an active service node, waiting until next round"); - context.state = round_state::prepare_for_round; - context.prepare_for_round.queue_for_next_round = true; - return event_loop::keep_running; + return goto_preparing_for_next_round(context); } service_nodes::payout block_producer_payouts = service_nodes::service_node_info_to_payout(key.pub, *info); @@ -867,7 +924,71 @@ event_loop wait_for_block_template(round_context &context) MINFO(log_prefix(context) << "Timed out, block template was not received"); } + context.state = round_state::submit_random_value_hash; + return event_loop::keep_running; + } + + return event_loop::return_to_caller; +} + +event_loop submit_random_value_hash(round_context &context, void *quorumnet_state, service_nodes::service_node_keys const &key) +{ + assert(context.prepare_for_round.participant == sn_type::validator); + + // Random Value + crypto::generate_random_bytes_thread_safe(sizeof(context.submit_random_value_hash.value.data), context.submit_random_value_hash.value.data); + + // Message + pulse::message msg = {}; + msg.type = pulse::message_type::random_value_hash; + msg.quorum_position = context.prepare_for_round.my_quorum_position; + msg.random_value_hash.hash = crypto::cn_fast_hash(context.submit_random_value_hash.value.data, sizeof(context.submit_random_value_hash.value.data)); + crypto::generate_signature(make_message_signature_hash(context, msg), key.pub, key.key, msg.signature); + + // Send + cryptonote::quorumnet_pulse_relay_message_to_quorum(quorumnet_state, msg, context.prepare_for_round.quorum, false /*block_producer*/); + context.state = round_state::wait_for_random_value_hashes; + return event_loop::return_to_caller; +} + +event_loop wait_for_random_value_hashes(round_context &context) +{ + bool timed_out = pulse::clock::now() >= context.wait_for_block_template.end_time; + + int received_hashes = 0; + int expected_hashes = 0; + uint16_t received_hashes_bitset = 0; + uint16_t validator_bitset = context.wait_for_block_template.block.pulse.validator_bitset; + for (size_t i = 0; i < service_nodes::PULSE_QUORUM_NUM_VALIDATORS; i++) + { + auto &[hash, received] = context.wait_for_random_value_hashes.hashes[i]; + uint16_t bit = (1 << i); + + if (validator_bitset & bit) // This validator is locked in for this round + { + received_hashes_bitset |= bit; + received_hashes += received; + expected_hashes++; + } + } + + if (expected_hashes == 0) + { + auto block_bitset = std::bitset(context.wait_for_block_template.block.pulse.validator_bitset); + auto our_bitset = std::bitset(context.submit_block_template.validator_bitset); + MERROR(log_prefix(context) << "Internal error, unexpected block validator bitset is empty " << block_bitset << ", our bitset was " << our_bitset); + return event_loop::keep_running; + } + + // TODO(doyle): Does this need to be == expected hashes or can it just be > min validator nodes for signing, i.e. 7. + if (timed_out || received_hashes == expected_hashes) + { + if (timed_out && received_hashes != expected_hashes) + return goto_preparing_for_next_round(context); + context.state = round_state::wait_for_next_block; + auto received_bitset = std::bitset(context.submit_block_template.validator_bitset); + MINFO(log_prefix(context) << "Received " << received_hashes << " random value hashes from " << received_bitset << (timed_out ? ". We timed out and some hashes are missing" : "")); return event_loop::keep_running; } @@ -937,6 +1058,14 @@ void pulse::main(void *quorumnet_state, cryptonote::core &core) case round_state::wait_for_block_template: loop = wait_for_block_template(context); break; + + case round_state::submit_random_value_hash: + loop = submit_random_value_hash(context, quorumnet_state, key); + break; + + case round_state::wait_for_random_value_hashes: + loop = wait_for_random_value_hashes(context); + break; } } } diff --git a/src/cryptonote_core/pulse.h b/src/cryptonote_core/pulse.h index cc9166e7a..861ec25b7 100644 --- a/src/cryptonote_core/pulse.h +++ b/src/cryptonote_core/pulse.h @@ -32,6 +32,7 @@ enum struct message_type : uint8_t handshake, handshake_bitset, block_template, + random_value_hash, }; constexpr std::string_view message_type_string(message_type type) @@ -42,6 +43,7 @@ constexpr std::string_view message_type_string(message_type type) case message_type::handshake: return "Handshake"sv; case message_type::handshake_bitset: return "Handshake Bitset"sv; case message_type::block_template: return "Block Template"sv; + case message_type::random_value_hash: return "Random Value Hash"sv; } return "Invalid2"sv; } @@ -61,6 +63,11 @@ struct message { std::string blob; } block_template; + + struct + { + crypto::hash hash; + } random_value_hash; }; void main(void *quorumnet_state, cryptonote::core &core); diff --git a/src/cryptonote_core/service_node_rules.h b/src/cryptonote_core/service_node_rules.h index 3226684aa..e37ef58f0 100644 --- a/src/cryptonote_core/service_node_rules.h +++ b/src/cryptonote_core/service_node_rules.h @@ -9,24 +9,19 @@ 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. #if defined(LOKI_ENABLE_INTEGRATION_TEST_HOOKS) constexpr auto PULSE_ROUND_TIME = 20s; - constexpr auto PULSE_WAIT_FOR_HANDSHAKES_DURATION = 7s; - constexpr auto PULSE_WAIT_FOR_OTHER_VALIDATOR_HANDSHAKES_DURATION = 7s; - constexpr auto PULSE_WAIT_FOR_BLOCK_TEMPLATE_DURATION = 6s; + constexpr auto PULSE_WAIT_FOR_HANDSHAKES_DURATION = 5s; + constexpr auto PULSE_WAIT_FOR_OTHER_VALIDATOR_HANDSHAKES_DURATION = 5s; + constexpr auto PULSE_WAIT_FOR_BLOCK_TEMPLATE_DURATION = 5s; + constexpr auto PULSE_WAIT_FOR_RANDOM_VALUE_HASH_DURATION = 5s; constexpr size_t PULSE_QUORUM_NUM_VALIDATORS = 0; constexpr size_t PULSE_BLOCK_REQUIRED_SIGNATURES = 0; #else -#if 0 constexpr auto PULSE_ROUND_TIME = 60s; - constexpr auto PULSE_WAIT_FOR_HANDSHAKES_DURATION = 20s; - constexpr auto PULSE_WAIT_FOR_OTHER_VALIDATOR_HANDSHAKES_DURATION = 20s; - constexpr auto PULSE_WAIT_FOR_BLOCK_TEMPLATE_DURATION = 20s; -#else - constexpr auto PULSE_ROUND_TIME = 20s; - constexpr auto PULSE_WAIT_FOR_HANDSHAKES_DURATION = 7s; - constexpr auto PULSE_WAIT_FOR_OTHER_VALIDATOR_HANDSHAKES_DURATION = 7s; - constexpr auto PULSE_WAIT_FOR_BLOCK_TEMPLATE_DURATION = 6s; -#endif + constexpr auto PULSE_WAIT_FOR_HANDSHAKES_DURATION = 15s; + constexpr auto PULSE_WAIT_FOR_OTHER_VALIDATOR_HANDSHAKES_DURATION = 15s; + constexpr auto PULSE_WAIT_FOR_BLOCK_TEMPLATE_DURATION = 15s; + constexpr auto PULSE_WAIT_FOR_RANDOM_VALUE_HASH_DURATION = 15s; constexpr size_t PULSE_QUORUM_NUM_VALIDATORS = 7; constexpr size_t PULSE_BLOCK_REQUIRED_SIGNATURES = 7; // A block must have exactly N signatures to be considered properly diff --git a/src/cryptonote_protocol/quorumnet.cpp b/src/cryptonote_protocol/quorumnet.cpp index edbc55556..632440581 100644 --- a/src/cryptonote_protocol/quorumnet.cpp +++ b/src/cryptonote_protocol/quorumnet.cpp @@ -1084,7 +1084,7 @@ void extract_signature_values(bt_dict_consumer& data, std::string_view key, std: crypto::signature convert_string_view_bytes_to_signature(std::string_view sig_str) { if (sig_str.size() != sizeof(crypto::signature)) - throw std::invalid_argument("Invalid signature data size: " + std::to_string(sizeof(crypto::signature))); + throw std::invalid_argument("Invalid signature data size: " + std::to_string(sig_str.size())); crypto::signature result; std::memcpy(&result, sig_str.data(), sizeof(crypto::signature)); @@ -1421,18 +1421,21 @@ void handle_blink_success(Message& m) { // // Pulse // -const std::string PULSE_TAG_VALIDATOR_BITSET = "b"; -const std::string PULSE_TAG_QUORUM_POSITION = "q"; -const std::string PULSE_TAG_SIGNATURE = "s"; -const std::string PULSE_TAG_BLOCK_TEMPLATE = "t"; +const std::string PULSE_TAG_VALIDATOR_BITSET = "b"; +const std::string PULSE_TAG_QUORUM_POSITION = "q"; +const std::string PULSE_TAG_SIGNATURE = "s"; +const std::string PULSE_TAG_BLOCK_TEMPLATE = "t"; +const std::string PULSE_TAG_RANDOM_VALUE_HASH = "#"; -const std::string PULSE_CMD_CATEGORY = "pulse"; -const std::string PULSE_CMD_VALIDATOR_BITSET = "validator_bitset"; -const std::string PULSE_CMD_VALIDATOR_BIT = "validator_bit"; -const std::string PULSE_CMD_BLOCK_TEMPLATE = "block_template"; -const std::string PULSE_CMD_SEND_VALIDATOR_BITSET = PULSE_CMD_CATEGORY + "." + PULSE_CMD_VALIDATOR_BITSET; -const std::string PULSE_CMD_SEND_VALIDATOR_BIT = PULSE_CMD_CATEGORY + "." + PULSE_CMD_VALIDATOR_BIT; -const std::string PULSE_CMD_SEND_BLOCK_TEMPLATE = PULSE_CMD_CATEGORY + "." + PULSE_CMD_BLOCK_TEMPLATE; +const std::string PULSE_CMD_CATEGORY = "pulse"; +const std::string PULSE_CMD_VALIDATOR_BITSET = "validator_bitset"; +const std::string PULSE_CMD_VALIDATOR_BIT = "validator_bit"; +const std::string PULSE_CMD_BLOCK_TEMPLATE = "block_template"; +const std::string PULSE_CMD_RANDOM_VALUE_HASH = "random_value_hash"; +const std::string PULSE_CMD_SEND_VALIDATOR_BITSET = PULSE_CMD_CATEGORY + "." + PULSE_CMD_VALIDATOR_BITSET; +const std::string PULSE_CMD_SEND_VALIDATOR_BIT = PULSE_CMD_CATEGORY + "." + PULSE_CMD_VALIDATOR_BIT; +const std::string PULSE_CMD_SEND_BLOCK_TEMPLATE = PULSE_CMD_CATEGORY + "." + PULSE_CMD_BLOCK_TEMPLATE; +const std::string PULSE_CMD_SEND_RANDOM_VALUE_HASH = PULSE_CMD_CATEGORY + "." + PULSE_CMD_RANDOM_VALUE_HASH; bt_dict pulse_serialize_message(pulse::message const &msg) { @@ -1440,7 +1443,6 @@ bt_dict pulse_serialize_message(pulse::message const &msg) switch(msg.type) { - default: case pulse::message_type::invalid: { assert(!"Invalid Code Path"); @@ -1468,6 +1470,14 @@ bt_dict pulse_serialize_message(pulse::message const &msg) {PULSE_TAG_BLOCK_TEMPLATE, msg.block_template.blob}}; } break; + + case pulse::message_type::random_value_hash: + { + result = {{PULSE_TAG_QUORUM_POSITION, msg.quorum_position}, + {PULSE_TAG_SIGNATURE, tools::view_guts(msg.signature)}, + {PULSE_TAG_RANDOM_VALUE_HASH, tools::view_guts(msg.random_value_hash.hash)}}; + } + break; } return result; @@ -1481,12 +1491,12 @@ void pulse_relay_message_to_quorum(void *self, pulse::message const &msg, servic std::string_view command = {}; switch(msg.type) { - default: break; + case pulse::message_type::invalid: + assert("Invalid Code Path" == nullptr); + break; case pulse::message_type::block_template: - { command = PULSE_CMD_SEND_BLOCK_TEMPLATE; - } break; case pulse::message_type::handshake: /* FALLTHRU */ @@ -1506,6 +1516,10 @@ void pulse_relay_message_to_quorum(void *self, pulse::message const &msg, servic } } break; + + case pulse::message_type::random_value_hash: + command = PULSE_CMD_SEND_RANDOM_VALUE_HASH; + break; } QnetState &qnet = *static_cast(self); @@ -1687,6 +1701,51 @@ void handle_pulse_block_template(Message &m, QnetState &qnet) qnet.lmq.job([&qnet, data = std::move(msg)]() { pulse::handle_message(&qnet, data); }, qnet.core.pulse_thread_id()); } +void handle_pulse_random_value_hash(Message &m, QnetState &qnet) +{ + if (m.data.size() != 1) + throw std::runtime_error(std::string("Rejecting pulse block template expected one data entry not ") + std::to_string(m.data.size())); + + bt_dict_consumer data{m.data[0]}; + + int quorum_position = -1; + crypto::hash random_value_hash = {}; + crypto::signature signature = {}; + { + std::string_view INVALID_ARG_PREFIX = "Invalid pulse random value hash: missing required field '"sv; + if (auto const &tag = PULSE_TAG_RANDOM_VALUE_HASH; data.skip_until(tag)) { + auto str = data.consume_string_view(); + if (str.size() != sizeof(random_value_hash)) + throw std::invalid_argument("Invalid hash data size: " + std::to_string(str.size())); + + std::memcpy(random_value_hash.data, str.data(), str.size()); + } else { + throw std::invalid_argument(std::string(INVALID_ARG_PREFIX) + std::string(tag) + "'"); + } + + if (auto const &tag = PULSE_TAG_QUORUM_POSITION; data.skip_until(tag)) + quorum_position = data.consume_integer(); + else + throw std::invalid_argument(std::string(INVALID_ARG_PREFIX) + std::string(tag) + "'"); + + if (auto const &tag = PULSE_TAG_SIGNATURE; data.skip_until(tag)) { + auto sig_str = data.consume_string_view(); + signature = convert_string_view_bytes_to_signature(sig_str); + } else { + throw std::invalid_argument(std::string(INVALID_ARG_PREFIX) + std::string(tag) + "'"); + } + } + + pulse::message msg = {}; + msg.type = pulse::message_type::random_value_hash; + msg.quorum_position = quorum_position; + msg.signature = signature; + msg.random_value_hash.hash = random_value_hash; + + auto *self = reinterpret_cast(&qnet); + qnet.lmq.job([self, data = std::move(msg)]() { pulse::handle_message(self, data); }, qnet.core.pulse_thread_id()); +} + } // end empty namespace @@ -1746,6 +1805,7 @@ void setup_endpoints(QnetState& qnet) { .add_command(PULSE_CMD_VALIDATOR_BIT, [&qnet](Message& m) { handle_pulse_participation_bit_or_bitset(m, qnet, false /*bitset*/); }) .add_command(PULSE_CMD_VALIDATOR_BITSET, [&qnet](Message& m) { handle_pulse_participation_bit_or_bitset(m, qnet, true /*bitset*/); }) .add_command(PULSE_CMD_BLOCK_TEMPLATE, [&qnet](Message& m) { handle_pulse_block_template(m, qnet); }) + .add_command(PULSE_CMD_RANDOM_VALUE_HASH, [&qnet](Message& m) { handle_pulse_random_value_hash(m, qnet); }) ; // Compatibility aliases. No longer used since 7.1.4, but can still be received from previous