diff --git a/src/blockchain/fork_choice.cpp b/src/blockchain/fork_choice.cpp index 46a1c5b..2d77ec7 100644 --- a/src/blockchain/fork_choice.cpp +++ b/src/blockchain/fork_choice.cpp @@ -12,12 +12,18 @@ #include #include +#include + #include "blockchain/genesis_config.hpp" #include "blockchain/is_proposer.hpp" #include "metrics/impl/metrics_impl.hpp" #include "types/signed_block_with_attestation.hpp" namespace lean { + inline auto attestationPayload(const AttestationData &attestation_data) { + return sszHash(attestation_data); + } + void ForkChoiceStore::updateSafeTarget() { // Get validator count from head state auto &head_state = getState(head_); @@ -142,19 +148,19 @@ namespace lean { outcome::result ForkChoiceStore::produceBlockWithSignatures(Slot slot, - ValidatorIndex validator_index) { + ValidatorIndex proposer_index) { // Get parent block and state to build upon const auto &head_root = getHead(); const auto &head_state = getState(head_root); // Validate proposer authorization for this slot - if (not isProposer(validator_index, slot, head_state.validatorCount())) { + if (not isProposer(proposer_index, slot, head_state.validatorCount())) { return Error::INVALID_PROPOSER; } // Initialize empty attestation set for iterative collection - Attestations attestations; - BlockSignatures signatures; + AggregatedAttestations aggregated_attestations; + std::unordered_map aggregated_attestation_indices; // Iteratively collect valid attestations using fixed-point algorithm // Continue until no new attestations can be added to the block @@ -162,11 +168,11 @@ namespace lean { // Create candidate block with current attestation set Block candidate_block{ .slot = slot, - .proposer_index = validator_index, + .proposer_index = proposer_index, .parent_root = head_root, // Temporary; updated after state computation .state_root = {}, - .body = {.attestations = attestations}, + .body = {.attestations = aggregated_attestations}, }; // Apply state transition to get the post-block state @@ -177,10 +183,15 @@ namespace lean { // Find new valid attestations matching post-state justification auto new_attestations = false; - for (auto &signed_attestation : - latest_known_attestations_ | std::views::values) { + for (auto &[validator_id, data] : latest_known_attestations_) { + auto signature_it = gossip_attestation_signatures_.find( + validatorAttestationKey(validator_id, data)); + if (signature_it == gossip_attestation_signatures_.end()) { + continue; + } + auto &signature = signature_it->second; + // Skip if target block is unknown in our store - auto &data = signed_attestation.message.data; if (not blocks_.contains(data.head.root)) { continue; } @@ -191,11 +202,26 @@ namespace lean { continue; } - if (not std::ranges::contains(attestations, - signed_attestation.message)) { + auto attestation_hash = attestationPayload(data); + auto attestation_index_it = + aggregated_attestation_indices.find(attestation_hash); + if (attestation_index_it == aggregated_attestation_indices.end()) { + attestation_index_it = + aggregated_attestation_indices + .emplace(attestation_hash, aggregated_attestations.size()) + .first; + aggregated_attestations.data().emplace_back( + AggregatedAttestation{.data = data}); + } + auto attestation_index = attestation_index_it->second; + auto &aggregated_attestation = + aggregated_attestations.data().at(attestation_index); + + if (not hasAggregatedValidator(aggregated_attestation.aggregation_bits, + validator_id)) { new_attestations = true; - attestations.push_back(signed_attestation.message); - signatures.push_back(signed_attestation.signature); + addAggregatedValidator(aggregated_attestation.aggregation_bits, + validator_id); } } @@ -208,11 +234,11 @@ namespace lean { // Create final block with all collected attestations Block block{ .slot = slot, - .proposer_index = validator_index, + .proposer_index = proposer_index, .parent_root = head_root, // Will be updated with computed hash .state_root = {}, - .body = {.attestations = attestations}, + .body = {.attestations = aggregated_attestations}, }; // Apply state transition to get final post-state and compute state root BOOST_OUTCOME_TRY(auto state, @@ -220,15 +246,33 @@ namespace lean { block.state_root = sszHash(state); block.setHash(); - auto proposer_attestation = produceAttestation(slot, validator_index); + AttestationSignatures aggregated_signatures; + for (auto &aggregated_attestation : aggregated_attestations) { + std::vector public_keys; + std::vector signatures; + for (auto &&validator_id : + getAggregatedValidators(aggregated_attestation.aggregation_bits)) { + public_keys.emplace_back( + state.validators.data().at(validator_id).pubkey); + signatures.emplace_back( + gossip_attestation_signatures_.at(validatorAttestationKey( + validator_id, aggregated_attestation.data))); + } + auto payload = attestationPayload(aggregated_attestation.data); + auto aggregated_signature = xmss_provider_->aggregateSignatures( + public_keys, signatures, aggregated_attestation.data.slot, payload); + aggregated_signatures.push_back(aggregated_signature); + } + + auto proposer_attestation = produceAttestation(slot, proposer_index); proposer_attestation.data.head = Checkpoint::from(block); // Sign proposer attestation - auto payload = sszHash(proposer_attestation); + auto payload = attestationPayload(proposer_attestation.data); auto timer = metrics_->crypto_pq_signature_attestation_signing_time_seconds() ->timer(); - crypto::xmss::XmssSignature signature = xmss_provider_->sign( + crypto::xmss::XmssSignature proposer_signature = xmss_provider_->sign( validator_keys_manifest_->currentNodeXmssKeypair().private_key, slot, payload); @@ -239,9 +283,12 @@ namespace lean { .block = block, .proposer_attestation = proposer_attestation, }, - .signature = signatures, + .signature = + { + .attestation_signatures = aggregated_signatures, + .proposer_signature = proposer_signature, + }, }; - signed_block_with_attestation.signature.data().push_back(signature); BOOST_OUTCOME_TRY(onBlock(signed_block_with_attestation)); return signed_block_with_attestation; @@ -256,8 +303,8 @@ namespace lean { } outcome::result ForkChoiceStore::validateAttestation( - const SignedAttestation &signed_attestation) { - auto &data = signed_attestation.message.data; + const Attestation &attestation) { + auto &data = attestation.data; SL_TRACE(logger_, "Validating attestation for target {}, source {}", @@ -308,11 +355,41 @@ namespace lean { return outcome::success(); } + outcome::result ForkChoiceStore::onGossipAttestation( + const SignedAttestation &signed_attestation) { + auto state_it = states_.find(signed_attestation.message.target.root); + if (state_it == states_.end()) { + return Error::INVALID_ATTESTATION; + } + auto &state = state_it->second; + if (signed_attestation.validator_id >= state.validators.size()) { + return Error::INVALID_ATTESTATION; + } + auto payload = attestationPayload(signed_attestation.message); + if (not xmss_provider_->verify( + state.validators[signed_attestation.validator_id].pubkey, + payload, + signed_attestation.message.slot, + signed_attestation.signature)) { + return Error::INVALID_ATTESTATION; + } + gossip_attestation_signatures_.emplace( + validatorAttestationKey(signed_attestation.validator_id, + signed_attestation.message), + signed_attestation.signature); + return onAttestation( + { + .validator_id = signed_attestation.validator_id, + .data = signed_attestation.message, + }, + false); + } + outcome::result ForkChoiceStore::onAttestation( - const SignedAttestation &signed_attestation, bool is_from_block) { + const Attestation &attestation, bool is_from_block) { // First, ensure the attestation is structurally and temporally valid. auto source = is_from_block ? "block" : "gossip"; - if (auto res = validateAttestation(signed_attestation); res.has_value()) { + if (auto res = validateAttestation(attestation); res.has_value()) { metrics_->fc_attestations_valid_total({{"source", source}})->inc(); } else { metrics_->fc_attestations_invalid_total({{"source", source}})->inc(); @@ -320,11 +397,11 @@ namespace lean { } // Extract the validator index that produced this attestation. - auto &validator_id = signed_attestation.message.validator_id; + auto &validator_id = attestation.validator_id; // Extract the attestation's slot: // - used to decide if this attestation is "newer" than a previous one. - auto &attestation_slot = signed_attestation.message.data.slot; + auto &attestation_slot = attestation.data.slot; if (is_from_block) { // On-chain attestation processing @@ -342,10 +419,9 @@ namespace lean { // - there is no known attestation yet, or // - this attestation is from a later slot than the known one. if (latest_known_attestation == latest_known_attestations_.end() - or latest_known_attestation->second.message.data.slot - < attestation_slot) { + or latest_known_attestation->second.slot < attestation_slot) { latest_known_attestations_.insert_or_assign(validator_id, - signed_attestation); + attestation.data); } // Fetch any pending ("new") attestation for this validator. @@ -357,8 +433,7 @@ namespace lean { // // In that case, the on-chain attestation supersedes it. if (latest_new_attestation != latest_new_attestations_.end() - and latest_new_attestation->second.message.data.slot - <= attestation_slot) { + and latest_new_attestation->second.slot <= attestation_slot) { latest_new_attestations_.erase(latest_new_attestation); } } else { @@ -385,10 +460,9 @@ namespace lean { // - there is no pending attestation yet, or // - this one is from a later slot than the pending one. if (latest_new_attestation == latest_new_attestations_.end() - or latest_new_attestation->second.message.data.slot - < attestation_slot) { + or latest_new_attestation->second.slot < attestation_slot) { latest_new_attestations_.insert_or_assign(validator_id, - signed_attestation); + attestation.data); } } @@ -411,8 +485,9 @@ namespace lean { // This creates a single list containing both: // 1. Block body attestations (from other validators) // 2. Proposer attestation (from the block producer) - auto all_attestations = block.body.attestations; - all_attestations.push_back(message.proposer_attestation); + auto &aggregated_attestations = block.body.attestations; + auto &attestation_signatures = + signed_block.signature.attestation_signatures; // Verify signature count matches attestation count // @@ -421,7 +496,7 @@ namespace lean { // The ordering must be preserved: // 1. Block body attestations, // 2. The proposer attestation. - if (signatures.size() != all_attestations.size()) { + if (attestation_signatures.size() != aggregated_attestations.size()) { SL_WARN(logger_, "Number of signatures does not match number of attestations"); return false; @@ -442,19 +517,17 @@ namespace lean { const auto &validators = parent_state.validators; // Verify each attestation signature - for (size_t index = 0; index < all_attestations.size(); ++index) { - const auto &attestation = all_attestations[index]; - const auto &signature = signatures.data().at(index); - - // Identify the validator who created this attestation - ValidatorIndex validator_id = attestation.validator_id; - - // Ensure validator exists in the active set - if (validator_id >= validators.size()) { - SL_WARN(logger_, "Validator index out of range"); - return false; + for (auto &&[aggregated_attestation, aggregated_signature] : + std::views::zip(aggregated_attestations, attestation_signatures)) { + std::vector public_keys; + for (auto &&validator_id : + getAggregatedValidators(aggregated_attestation.aggregation_bits)) { + if (validator_id >= validators.size()) { + SL_WARN(logger_, "Validator index out of range"); + return false; + } + public_keys.emplace_back(validators.data().at(validator_id).pubkey); } - const auto &validator = validators[validator_id]; // Verify the XMSS signature // @@ -462,23 +535,49 @@ namespace lean { // - The validator possesses the secret key for their public key // - The attestation has not been tampered with // - The signature was created at the correct epoch (slot) - auto message = sszHash(attestation); - Epoch epoch = attestation.data.slot; + auto message = attestationPayload(aggregated_attestation.data); + Epoch epoch = aggregated_attestation.data.slot; auto timer = - metrics_->crypto_pq_signature_attestation_verification_time_seconds() + metrics_ + ->crypto_pq_signature_aggregated_attestation_verification_time_seconds() ->timer(); - bool verify_result = - xmss_provider_->verify(validator.pubkey, message, epoch, signature); + bool verify_result = xmss_provider_->verifyAggregatedSignatures( + public_keys, + epoch, + message, + qtils::ByteVec{aggregated_signature.data()}); timer.stop(); if (not verify_result) { SL_WARN(logger_, - "Attestation signature verification failed for validator {}", - validator_id); + "Attestation signature verification failed for validators {}", + fmt::join(getAggregatedValidators( + aggregated_attestation.aggregation_bits), + " ")); return false; } } + + auto payload = attestationPayload(message.proposer_attestation.data); + auto timer = + metrics_->crypto_pq_signature_attestation_verification_time_seconds() + ->timer(); + bool verify_result = xmss_provider_->verify( + parent_state.validators.data() + .at(message.proposer_attestation.validator_id) + .pubkey, + payload, + message.proposer_attestation.data.slot, + signed_block.signature.proposer_signature); + timer.stop(); + + if (not verify_result) { + SL_WARN(logger_, + "Attestation signature verification failed for validator {}", + message.proposer_attestation.validator_id); + return false; + } return true; } @@ -530,22 +629,37 @@ namespace lean { states_.emplace(block_hash, std::move(post_state)); // Process block body attestations - // - // Iterate over attestations and their corresponding signatures. - for (size_t index = 0; index < block.body.attestations.size(); ++index) { - if (index >= signatures.size()) { - return Error::INVALID_ATTESTATION; + auto &aggregated_attestations = + signed_block_with_attestation.message.block.body.attestations; + auto &attestation_signatures = + signed_block_with_attestation.signature.attestation_signatures; + if (attestation_signatures.size() != aggregated_attestations.size()) { + return Error::SIGNATURE_COUNT_MISMATCH; + } + for (auto &&[aggregated_attestation, aggregated_signature] : + std::views::zip(aggregated_attestations, attestation_signatures)) { + auto shared_aggregated_signature = + qtils::toSharedPtr(aggregated_signature); + for (auto &&validator_id : + getAggregatedValidators(aggregated_attestation.aggregation_bits)) { + // Store the aggregated signature payload against (validator_id, + // data_root) This is a list because the same (validator_id, data) can + // appear in multiple aggregated attestations, especially when we have + // aggregator roles. This list can be recursively aggregated by the + // block proposer. + block_attestation_signatures_[validatorAttestationKey( + validator_id, + aggregated_attestation.data)] + .emplace_back(shared_aggregated_signature); + + // Import the attestation data into forkchoice for latest votes + BOOST_OUTCOME_TRY(onAttestation( + Attestation{ + .validator_id = validator_id, + .data = aggregated_attestation.data, + }, + true)); } - const auto &attestation = block.body.attestations[index]; - const auto &signature = signatures.data().at(index); - - // Process as on-chain attestation (immediately becomes "known") - BOOST_OUTCOME_TRY(onAttestation( - SignedAttestation{ - .message = attestation, - .signature = signature, - }, - true)); } // Update forkchoice head based on new block and attestations @@ -561,10 +675,16 @@ namespace lean { // 1. NOT affect this block's fork choice position (processed as "new") // 2. Be available for inclusion in future blocks // 3. Influence fork choice only after interval 3 (end of slot) + // We also store the proposer's signature for potential future block + // building. + gossip_attestation_signatures_.emplace( + validatorAttestationKey(proposer_attestation.validator_id, + proposer_attestation.data), + signed_block_with_attestation.signature.proposer_signature); BOOST_OUTCOME_TRY(onAttestation( - SignedAttestation{ - .message = proposer_attestation, - .signature = signatures.data().at(block.body.attestations.size()), + Attestation{ + .validator_id = proposer_attestation.validator_id, + .data = proposer_attestation.data, }, false)); @@ -642,7 +762,7 @@ namespace lean { } auto attestation = produceAttestation(current_slot, validator_index); // sign attestation - auto payload = sszHash(attestation); + auto payload = attestationPayload(attestation.data); crypto::xmss::XmssKeypair keypair = validator_keys_manifest_->currentNodeXmssKeypair(); auto timer = @@ -651,12 +771,15 @@ namespace lean { crypto::xmss::XmssSignature signature = xmss_provider_->sign(keypair.private_key, current_slot, payload); timer.stop(); - SignedAttestation signed_attestation{.message = attestation, - .signature = signature}; + SignedAttestation signed_attestation{ + .validator_id = validator_index, + .message = attestation.data, + .signature = signature, + }; // Dispatching send signed vote only broadcasts to other peers. // Current peer should process attestation directly - auto res = onAttestation(signed_attestation, false); + auto res = onAttestation(attestation, false); if (not res.has_value()) { SL_ERROR(logger_, "Failed to process attestation for slot {}: {}", @@ -664,9 +787,8 @@ namespace lean { res.error()); continue; } - SL_INFO(logger_, - "Produced vote for target {}", - signed_attestation.message.data.target); + SL_INFO( + logger_, "Produced vote for target {}", attestation.data.target); result.emplace_back(std::move(signed_attestation)); } } else if (time_ % INTERVALS_PER_SLOT == 2) { @@ -692,7 +814,7 @@ namespace lean { BlockHash ForkChoiceStore::computeLmdGhostHead( const BlockHash &start_root, - const SignedAttestations &attestations, + const AttestationDataByValidator &attestations, uint64_t min_score) const { BOOST_ASSERT(not blocks_.empty()); @@ -729,7 +851,7 @@ namespace lean { // // Each visited block accumulates one unit of weight from that validator. for (auto &attestation : attestations | std::views::values) { - auto current = attestation.message.data.head.root; + auto current = attestation.head.root; // Climb towards the anchor while staying inside the known tree. // @@ -867,8 +989,8 @@ namespace lean { Checkpoint latest_finalized, Blocks blocks, std::unordered_map states, - SignedAttestations latest_known_attestations, - SignedAttestations latest_new_attestations, + AttestationDataByValidator latest_known_attestations, + AttestationDataByValidator latest_new_attestations, ValidatorIndex validator_index, qtils::SharedRef validator_registry, qtils::SharedRef validator_keys_manifest, @@ -890,4 +1012,13 @@ namespace lean { validator_registry_(std::move(validator_registry)), validator_keys_manifest_(std::move(validator_keys_manifest)), xmss_provider_(std::move(xmss_provider)) {} + + ForkChoiceStore::ValidatorAttestationKey + ForkChoiceStore::validatorAttestationKey( + ValidatorIndex validator_index, const AttestationData &attestation_data) { + return { + validator_index, + sszHash(attestation_data), + }; + } } // namespace lean diff --git a/src/blockchain/fork_choice.hpp b/src/blockchain/fork_choice.hpp index 4bc68eb..388faa9 100644 --- a/src/blockchain/fork_choice.hpp +++ b/src/blockchain/fork_choice.hpp @@ -26,6 +26,7 @@ #include "types/state.hpp" #include "types/validator_index.hpp" #include "utils/ceil_div.hpp" +#include "utils/tuple_hash.hpp" namespace lean { struct GenesisConfig; @@ -56,12 +57,13 @@ namespace lean { class ForkChoiceStore { public: using Blocks = std::unordered_map; - using SignedAttestations = - std::unordered_map; + using AttestationDataByValidator = + std::unordered_map; enum class Error { INVALID_ATTESTATION, INVALID_PROPOSER, + SIGNATURE_COUNT_MISMATCH, }; Q_ENUM_ERROR_CODE_FRIEND(Error) { using E = decltype(e); @@ -70,6 +72,8 @@ namespace lean { return "Invalid attestation"; case E::INVALID_PROPOSER: return "Invalid proposer"; + case E::SIGNATURE_COUNT_MISMATCH: + return "Signature count must match attestation count"; } abort(); } @@ -111,8 +115,8 @@ namespace lean { Checkpoint latest_finalized, Blocks blocks, std::unordered_map states, - SignedAttestations latest_known_attestations, - SignedAttestations latest_new_votes, + AttestationDataByValidator latest_known_attestations, + AttestationDataByValidator latest_new_votes, ValidatorIndex validator_index, qtils::SharedRef validator_registry, qtils::SharedRef validator_keys_manifest, @@ -151,13 +155,13 @@ namespace lean { const Blocks &getBlocks() const { return blocks_; } - const SignedAttestations &getLatestNewAttestations() const { + const AttestationDataByValidator &getLatestNewAttestations() const { return latest_new_attestations_; } - const SignedAttestations &getLatestKnownAttestations() const { + const AttestationDataByValidator &getLatestKnownAttestations() const { return latest_known_attestations_; } - SignedAttestations &getLatestNewVotesRef() { + AttestationDataByValidator &getLatestNewVotesRef() { return latest_new_attestations_; } @@ -193,9 +197,10 @@ namespace lean { * Returns: * Hash of the chosen head block. */ - BlockHash computeLmdGhostHead(const BlockHash &start_root, - const SignedAttestations &attestations, - uint64_t min_score = 0) const; + BlockHash computeLmdGhostHead( + const BlockHash &start_root, + const AttestationDataByValidator &attestations, + uint64_t min_score = 0) const; /** * Calculate target checkpoint for validator attestations. @@ -307,7 +312,18 @@ namespace lean { * Returns: * Success if validation passes, error otherwise. */ - outcome::result validateAttestation( + outcome::result validateAttestation(const Attestation &attestation); + + /** + * Process a signed attestation received via gossip network. + * This method: + * 1. Verifies the XMSS signature + * 2. Stores the signature in the gossip signature map + * 3. Processes the attestation data via on_attestation + * Args: + * signed_attestation: The signed attestation from gossip. + */ + outcome::result onGossipAttestation( const SignedAttestation &signed_attestation); /** @@ -356,8 +372,8 @@ namespace lean { * Returns: * Success if the attestation was processed, error otherwise. */ - outcome::result onAttestation( - const SignedAttestation &signed_attestation, bool is_from_block); + outcome::result onAttestation(const Attestation &attestation, + bool is_from_block); // Processes a new block, updates the store, and triggers a head update. @@ -377,6 +393,12 @@ namespace lean { } private: + using ValidatorAttestationKey = std::tuple; + + static ValidatorAttestationKey validatorAttestationKey( + ValidatorIndex validator_index, + const AttestationData &attestation_data); + // Verify all XMSS signatures in a signed block. // // This method ensures that every attestation included in the block @@ -464,7 +486,7 @@ namespace lean { * For each validator, stores their most recent attestation that is * currently influencing the fork choice head computation. */ - SignedAttestations latest_known_attestations_; + AttestationDataByValidator latest_known_attestations_; /** * Pending attestations awaiting activation. @@ -477,7 +499,24 @@ namespace lean { * Attestations move from this map to latest_known_attestations_ during * interval ticks. */ - SignedAttestations latest_new_attestations_; + AttestationDataByValidator latest_new_attestations_; + /** + * Map of validator id and attestation root to the XMSS signature. + */ + std::unordered_map + gossip_attestation_signatures_; + /** + * Aggregated signature payloads for attestations from blocks. + * - Keyed by (validator_id, attestation_data_root). + * - Values are lists because same (validator_id, data) can appear in + * multiple aggregations. + * - Used for recursive signature aggregation when building blocks. + * - Populated by on_block. + */ + std::unordered_map>> + block_attestation_signatures_; + qtils::SharedRef validator_registry_; qtils::SharedRef validator_keys_manifest_; log::Logger logger_; diff --git a/src/blockchain/state_transition_function.cpp b/src/blockchain/state_transition_function.cpp index 468a170..e2bc942 100644 --- a/src/blockchain/state_transition_function.cpp +++ b/src/blockchain/state_transition_function.cpp @@ -240,7 +240,7 @@ namespace lean { } outcome::result STF::processAttestations( - State &state, const Attestations &attestations) const { + State &state, const AggregatedAttestations &attestations) const { auto timer = metrics_->stf_attestations_processing_time_seconds()->timer(); // NOTE: @@ -346,12 +346,14 @@ namespace lean { justifications_it->second.resize(state.validatorCount()); } - auto validator_id = attestation.validator_id; - if (validator_id >= justifications_it->second.size()) { - return Error::INVALID_VOTER; - } - if (not justifications_it->second.at(validator_id)) { - justifications_it->second.at(validator_id) = true; + for (auto &&validator_id : + getAggregatedValidators(attestation.aggregation_bits)) { + if (validator_id >= justifications_it->second.size()) { + return Error::INVALID_VOTER; + } + if (not justifications_it->second.at(validator_id)) { + justifications_it->second.at(validator_id) = true; + } } size_t count = std::ranges::count(justifications_it->second, true); diff --git a/src/blockchain/state_transition_function.hpp b/src/blockchain/state_transition_function.hpp index 4dee375..fb7774f 100644 --- a/src/blockchain/state_transition_function.hpp +++ b/src/blockchain/state_transition_function.hpp @@ -85,7 +85,7 @@ namespace lean { outcome::result processOperations(State &state, const BlockBody &body) const; outcome::result processAttestations( - State &state, const Attestations &attestations) const; + State &state, const AggregatedAttestations &attestations) const; bool validateProposerIndex(const State &state, const Block &block) const; private: diff --git a/src/crypto/crypto_metrics.def b/src/crypto/crypto_metrics.def index d7a401d..cbce713 100644 --- a/src/crypto/crypto_metrics.def +++ b/src/crypto/crypto_metrics.def @@ -47,3 +47,7 @@ METRIC_HISTOGRAM(crypto_pq_signature_attestation_verification_time_seconds, "lean_pq_signature_attestation_verification_time_seconds", "Time taken to verify an attestation signature", (0.005, 0.01, 0.025, 0.05, 0.1, 1)) +METRIC_HISTOGRAM(crypto_pq_signature_aggregated_attestation_verification_time_seconds, + "TODO_lean_pq_signature_aggregated_attestation_verification_time_seconds", + "TODO Time taken to verify an aggregated attestation signature", + (0.005, 0.01, 0.025, 0.05, 0.1, 1)) diff --git a/src/crypto/xmss/types.hpp b/src/crypto/xmss/types.hpp index 194a402..3a96a8d 100644 --- a/src/crypto/xmss/types.hpp +++ b/src/crypto/xmss/types.hpp @@ -18,5 +18,7 @@ namespace lean::crypto::xmss { XmssPrivateKey private_key; XmssPublicKey public_key; }; + using XmssMessage = qtils::ByteArr; using XmssSignature = qtils::ByteArr; + using XmssAggregatedSignature = qtils::ByteVec; } // namespace lean::crypto::xmss diff --git a/src/crypto/xmss/xmss_provider.hpp b/src/crypto/xmss/xmss_provider.hpp index 9afdba1..f14cba8 100644 --- a/src/crypto/xmss/xmss_provider.hpp +++ b/src/crypto/xmss/xmss_provider.hpp @@ -19,11 +19,23 @@ namespace lean::crypto::xmss { virtual XmssSignature sign(XmssPrivateKey xmss_private_key, uint32_t epoch, - qtils::BytesIn message) = 0; + const XmssMessage &message) = 0; - virtual bool verify(XmssPublicKey xmss_public_key, - qtils::BytesIn message, + virtual bool verify(const XmssPublicKey &xmss_public_key, + const XmssMessage &message, uint32_t epoch, - XmssSignature xmss_signature) = 0; + const XmssSignature &xmss_signature) = 0; + + virtual XmssAggregatedSignature aggregateSignatures( + std::span public_keys, + std::span signatures, + uint32_t epoch, + const XmssMessage &message) const = 0; + + virtual bool verifyAggregatedSignatures( + std::span public_keys, + uint32_t epoch, + const XmssMessage &message, + const XmssAggregatedSignature &aggregated_signature) const = 0; }; } // namespace lean::crypto::xmss diff --git a/src/crypto/xmss/xmss_provider_impl.cpp b/src/crypto/xmss/xmss_provider_impl.cpp index 1060862..9a68728 100644 --- a/src/crypto/xmss/xmss_provider_impl.cpp +++ b/src/crypto/xmss/xmss_provider_impl.cpp @@ -56,7 +56,7 @@ namespace lean::crypto::xmss { XmssSignature XmssProviderImpl::sign(XmssPrivateKey xmss_private_key, uint32_t epoch, - qtils::BytesIn message) { + const XmssMessage &message) { // Sign the message PQSignature *signature_raw = nullptr; ffi::asOutcome( @@ -71,10 +71,10 @@ namespace lean::crypto::xmss { return signature_bytes; } - bool XmssProviderImpl::verify(XmssPublicKey xmss_public_key, - qtils::BytesIn message, + bool XmssProviderImpl::verify(const XmssPublicKey &xmss_public_key, + const XmssMessage &message, uint32_t epoch, - XmssSignature xmss_signature) { + const XmssSignature &xmss_signature) { // Deserialize public key PQPublicKey *public_key_raw = nullptr; ffi::asOutcome( @@ -100,4 +100,51 @@ namespace lean::crypto::xmss { return verify_result == 1; } + auto manyToRaw(const auto &items) { + std::vector items_raw; + items_raw.reserve(items.size()); + for (auto &item : items) { + items_raw.emplace_back(item.data()); + } + return items_raw; + } + + XmssAggregatedSignature XmssProviderImpl::aggregateSignatures( + std::span public_keys, + std::span signatures, + uint32_t epoch, + const XmssMessage &message) const { + if (public_keys.size() != signatures.size()) { + throw std::logic_error{ + "XmssProviderImpl::aggregateSignatures public key and signature " + "count mismatch"}; + } + auto public_keys_raw = manyToRaw(public_keys); + auto signatures_raw = manyToRaw(signatures); + auto ffi_bytevec = pq_aggregate_signatures(public_keys.size(), + public_keys_raw.data(), + signatures_raw.data(), + epoch, + message.data()); + XmssAggregatedSignature aggregated_signature{std::span{ + ffi_bytevec.ptr, + ffi_bytevec.size, + }}; + PQByteVec_drop(ffi_bytevec); + return aggregated_signature; + } + + bool XmssProviderImpl::verifyAggregatedSignatures( + std::span public_keys, + uint32_t epoch, + const XmssMessage &message, + const XmssAggregatedSignature &aggregated_signature) const { + auto public_keys_raw = manyToRaw(public_keys); + return pq_verify_aggregated_signatures(public_keys.size(), + public_keys_raw.data(), + epoch, + message.data(), + aggregated_signature.data(), + aggregated_signature.size()); + } } // namespace lean::crypto::xmss diff --git a/src/crypto/xmss/xmss_provider_impl.hpp b/src/crypto/xmss/xmss_provider_impl.hpp index bb164b5..1c7b9ef 100644 --- a/src/crypto/xmss/xmss_provider_impl.hpp +++ b/src/crypto/xmss/xmss_provider_impl.hpp @@ -17,12 +17,23 @@ namespace lean::crypto::xmss { XmssSignature sign(XmssPrivateKey xmss_private_key, uint32_t epoch, - qtils::BytesIn message) override; + const XmssMessage &message) override; - bool verify(XmssPublicKey xmss_public_key, - qtils::BytesIn message, + bool verify(const XmssPublicKey &xmss_public_key, + const XmssMessage &message, uint32_t epoch, - XmssSignature xmss_signature) override; + const XmssSignature &xmss_signature) override; + + XmssAggregatedSignature aggregateSignatures( + std::span public_keys, + std::span signatures, + uint32_t epoch, + const XmssMessage &message) const override; + bool verifyAggregatedSignatures( + std::span public_keys, + uint32_t epoch, + const XmssMessage &message, + const XmssAggregatedSignature &aggregated_signature) const override; }; } // namespace lean::crypto::xmss diff --git a/src/modules/networking/networking.cpp b/src/modules/networking/networking.cpp index 68816a1..1fe830f 100644 --- a/src/modules/networking/networking.cpp +++ b/src/modules/networking/networking.cpp @@ -392,18 +392,18 @@ namespace lean::modules { if (not self) { return; } - auto res = self->fork_choice_store_->onAttestation(signed_attestation, - false); + auto res = + self->fork_choice_store_->onGossipAttestation(signed_attestation); if (not res.has_value()) { SL_WARN(self->logger_, "Error processing vote for target {}: {}", - signed_attestation.message.data.target, + signed_attestation.message.target, res.error()); return; } SL_DEBUG(self->logger_, "Received vote for target {}", - signed_attestation.message.data.target); + signed_attestation.message.target); }); io_thread_.emplace([io_context{io_context_}] { diff --git a/src/types/aggregated_attestation.hpp b/src/types/aggregated_attestation.hpp new file mode 100644 index 0000000..73ad42b --- /dev/null +++ b/src/types/aggregated_attestation.hpp @@ -0,0 +1,22 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "types/aggregation_bits.hpp" +#include "types/attestation_data.hpp" + +namespace lean { + struct AggregatedAttestation : ssz::ssz_variable_size_container { + AggregationBits aggregation_bits; + AttestationData data; + + SSZ_CONT(aggregation_bits, data); + bool operator==(const AggregatedAttestation &) const = default; + }; +} // namespace lean diff --git a/src/types/aggregated_attestations.hpp b/src/types/aggregated_attestations.hpp new file mode 100644 index 0000000..233afc1 --- /dev/null +++ b/src/types/aggregated_attestations.hpp @@ -0,0 +1,17 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "types/aggregated_attestation.hpp" +#include "types/constants.hpp" + +namespace lean { + using AggregatedAttestations = + ssz::list; +} // namespace lean diff --git a/src/types/aggregation_bits.hpp b/src/types/aggregation_bits.hpp new file mode 100644 index 0000000..cbd4498 --- /dev/null +++ b/src/types/aggregation_bits.hpp @@ -0,0 +1,40 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include + +#include "types/constants.hpp" +#include "types/validator_index.hpp" + +namespace lean { + using AggregationBits = ssz::list; + + inline auto getAggregatedValidators(const AggregationBits &aggregation_bits) { + return std::views::iota(ValidatorIndex{0}, + ValidatorIndex{aggregation_bits.size()}) + | std::views::filter([&](ValidatorIndex validator_index) { + return aggregation_bits.data().at(validator_index); + }); + } + + inline bool hasAggregatedValidator(const AggregationBits &aggregation_bits, + ValidatorIndex validator_index) { + return validator_index < aggregation_bits.size() + and aggregation_bits.data().at(validator_index); + } + + inline void addAggregatedValidator(AggregationBits &aggregation_bits, + ValidatorIndex validator_index) { + if (aggregation_bits.size() <= validator_index) { + aggregation_bits.data().resize(validator_index + 1); + } + aggregation_bits.data().at(validator_index) = true; + } +} // namespace lean diff --git a/src/types/attestations_signatures.hpp b/src/types/attestations_signatures.hpp new file mode 100644 index 0000000..34ec1c5 --- /dev/null +++ b/src/types/attestations_signatures.hpp @@ -0,0 +1,17 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "types/constants.hpp" +#include "types/lean_aggregated_signature.hpp" + +namespace lean { + using AttestationSignatures = + ssz::list; +} // namespace lean diff --git a/src/types/block_body.hpp b/src/types/block_body.hpp index 3dabf13..607679d 100644 --- a/src/types/block_body.hpp +++ b/src/types/block_body.hpp @@ -8,12 +8,11 @@ #include -#include "types/attestations.hpp" +#include "types/aggregated_attestations.hpp" namespace lean { struct BlockBody : ssz::ssz_variable_size_container { - /// @note attestations will be replaced by aggregated attestations. - Attestations attestations; + AggregatedAttestations attestations; SSZ_CONT(attestations); bool operator==(const BlockBody &) const = default; diff --git a/src/types/block_signatures.hpp b/src/types/block_signatures.hpp index 468257d..e505d36 100644 --- a/src/types/block_signatures.hpp +++ b/src/types/block_signatures.hpp @@ -6,11 +6,17 @@ #pragma once -#include +#include -#include "types/constants.hpp" +#include "types/attestations_signatures.hpp" #include "types/signature.hpp" namespace lean { - using BlockSignatures = ssz::list; + struct BlockSignatures : ssz::ssz_variable_size_container { + AttestationSignatures attestation_signatures; + Signature proposer_signature; + + SSZ_CONT(attestation_signatures, proposer_signature); + bool operator==(const BlockSignatures &) const = default; + }; } // namespace lean diff --git a/src/types/lean_aggregated_signature.hpp b/src/types/lean_aggregated_signature.hpp new file mode 100644 index 0000000..154c502 --- /dev/null +++ b/src/types/lean_aggregated_signature.hpp @@ -0,0 +1,13 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace lean { + using LeanAggregatedSignature = ssz::list; +} // namespace lean diff --git a/src/types/signed_attestation.hpp b/src/types/signed_attestation.hpp index fdd8fea..e3c18c5 100644 --- a/src/types/signed_attestation.hpp +++ b/src/types/signed_attestation.hpp @@ -8,14 +8,16 @@ #include -#include "types/attestation.hpp" +#include "types/attestation_data.hpp" #include "types/signature.hpp" +#include "types/validator_index.hpp" namespace lean { struct SignedAttestation : ssz::ssz_container { - Attestation message; + ValidatorIndex validator_id; + AttestationData message; Signature signature; - SSZ_CONT(message, signature); + SSZ_CONT(validator_id, message, signature); }; } // namespace lean diff --git a/src/types/signed_block_with_attestation.hpp b/src/types/signed_block_with_attestation.hpp index 59127ad..8707b8a 100644 --- a/src/types/signed_block_with_attestation.hpp +++ b/src/types/signed_block_with_attestation.hpp @@ -19,13 +19,4 @@ namespace lean { SSZ_CONT(message, signature); bool operator==(const SignedBlockWithAttestation &) const = default; }; - - /** - * Stub method to sign block. - */ - inline SignedBlockWithAttestation signBlock( - SignedBlockWithAttestation signed_block_with_attestation) { - signed_block_with_attestation.signature.data().emplace_back(); - return signed_block_with_attestation; - } } // namespace lean diff --git a/tests/mock/crypto/xmss_provider_mock.hpp b/tests/mock/crypto/xmss_provider_mock.hpp index c1aa3ce..d581ca7 100644 --- a/tests/mock/crypto/xmss_provider_mock.hpp +++ b/tests/mock/crypto/xmss_provider_mock.hpp @@ -13,17 +13,31 @@ namespace lean::crypto::xmss { class XmssProviderMock : public XmssProvider { public: - MOCK_METHOD(XmssKeypair, - generateKeypair, - (uint64_t, uint64_t), - (override)); + MOCK_METHOD(XmssKeypair, generateKeypair, (uint64_t, uint64_t), (override)); MOCK_METHOD(XmssSignature, sign, - (XmssPrivateKey, uint32_t, qtils::BytesIn), + (XmssPrivateKey, uint32_t, const XmssMessage &), (override)); MOCK_METHOD(bool, verify, - (XmssPublicKey, qtils::BytesIn, uint32_t, XmssSignature), + (const XmssPublicKey &, + const XmssMessage &, + uint32_t, + const XmssSignature &), (override)); + MOCK_METHOD(XmssAggregatedSignature, + aggregateSignatures, + (std::span, + std::span, + uint32_t, + const XmssMessage &), + (const, override)); + MOCK_METHOD(bool, + verifyAggregatedSignatures, + (std::span, + uint32_t, + const XmssMessage &, + const XmssAggregatedSignature &), + (const, override)); }; } // namespace lean::crypto::xmss diff --git a/tests/unit/blockchain/fork_choice_test.cpp b/tests/unit/blockchain/fork_choice_test.cpp index e83aa44..7bb3bdf 100644 --- a/tests/unit/blockchain/fork_choice_test.cpp +++ b/tests/unit/blockchain/fork_choice_test.cpp @@ -25,6 +25,7 @@ #include "qtils/test/outcome.hpp" #include "testutil/prepare_loggers.hpp" +using lean::Attestation; using lean::Block; using lean::Checkpoint; using lean::ForkChoiceStore; @@ -40,30 +41,26 @@ lean::BlockHash testHash(std::string_view s) { return hash; } -SignedAttestation makeAttestation(const Block &source, const Block &target) { - return SignedAttestation{ - .message = +Attestation makeAttestation(const Block &source, const Block &target) { + return Attestation{ + .validator_id = 0, + .data = { - .validator_id = 0, - .data = - { - .slot = target.slot, - .head = Checkpoint::from(target), - .target = Checkpoint::from(target), - .source = Checkpoint::from(source), - }, + .slot = target.slot, + .head = Checkpoint::from(target), + .target = Checkpoint::from(target), + .source = Checkpoint::from(source), }, - .signature = {}, }; } std::optional getAttestation( - const ForkChoiceStore::SignedAttestations &votes) { + const ForkChoiceStore::AttestationDataByValidator &votes) { auto it = votes.find(0); if (it == votes.end()) { return std::nullopt; } - return it->second.message.data.target; + return it->second.target; } lean::Config config{ @@ -79,8 +76,8 @@ auto createTestStore( lean::Checkpoint latest_finalized = {}, ForkChoiceStore::Blocks blocks = {}, std::unordered_map states = {}, - ForkChoiceStore::SignedAttestations latest_known_attestations = {}, - ForkChoiceStore::SignedAttestations latest_new_attestations = {}, + ForkChoiceStore::AttestationDataByValidator latest_known_attestations = {}, + ForkChoiceStore::AttestationDataByValidator latest_new_attestations = {}, lean::ValidatorIndex validator_index = 0) { auto validator_registry = std::make_shared(); static lean::ValidatorRegistry::ValidatorIndices validators{0}; @@ -298,20 +295,12 @@ TEST(TestForkChoiceHeadFunction, test_get_fork_choice_head_with_votes) { auto &root = blocks.at(0); auto &target = blocks.at(2); - ForkChoiceStore::SignedAttestations attestations; - attestations[0] = SignedAttestation{ - .message = - { - .validator_id = 0, - .data = - { - .slot = target.slot, - .head = Checkpoint::from(target), - .target = Checkpoint::from(target), - .source = Checkpoint::from(root), - }, - }, - .signature = {}, + ForkChoiceStore::AttestationDataByValidator attestations; + attestations[0] = { + .slot = target.slot, + .head = Checkpoint::from(target), + .target = Checkpoint::from(target), + .source = Checkpoint::from(root), }; auto store = createTestStore(100, @@ -338,7 +327,7 @@ TEST(TestForkChoiceHeadFunction, test_fork_choice_no_attestations) { auto &root = blocks.at(0); auto &leaf = blocks.at(2); - ForkChoiceStore::SignedAttestations empty_attestations; + ForkChoiceStore::AttestationDataByValidator empty_attestations; auto store = createTestStore(100, config, root.hash(), @@ -358,20 +347,12 @@ TEST(TestForkChoiceHeadFunction, test_get_fork_choice_head_with_min_score) { auto &root = blocks.at(0); auto &target = blocks.at(2); - ForkChoiceStore::SignedAttestations attestations; - attestations[0] = SignedAttestation{ - .message = - { - .validator_id = 0, - .data = - { - .slot = target.slot, - .head = Checkpoint::from(target), - .target = Checkpoint::from(target), - .source = Checkpoint::from(root), - }, - }, - .signature = {}, + ForkChoiceStore::AttestationDataByValidator attestations; + attestations[0] = { + .slot = target.slot, + .head = Checkpoint::from(target), + .target = Checkpoint::from(target), + .source = Checkpoint::from(root), }; auto store = createTestStore(100, @@ -393,21 +374,13 @@ TEST(TestForkChoiceHeadFunction, test_get_fork_choice_head_multiple_votes) { auto &root = blocks.at(0); auto &target = blocks.at(2); - ForkChoiceStore::SignedAttestations attestations; + ForkChoiceStore::AttestationDataByValidator attestations; for (int i = 0; i < 3; ++i) { - attestations[i] = SignedAttestation{ - .message = - { - .validator_id = static_cast(i), - .data = - { - .slot = target.slot, - .head = Checkpoint::from(target), - .target = Checkpoint::from(target), - .source = Checkpoint::from(root), - }, - }, - .signature = {}, + attestations[i] = { + .slot = target.slot, + .head = Checkpoint::from(target), + .target = Checkpoint::from(target), + .source = Checkpoint::from(root), }; } @@ -496,7 +469,7 @@ TEST(TestAttestationValidation, // Create signed vote with mismatched checkpoint slot auto attestation = makeAttestation(source, target); - ++attestation.message.data.source.slot; + ++attestation.data.source.slot; EXPECT_OUTCOME_ERROR(sample_store.validateAttestation(attestation)); } diff --git a/tests/unit/crypto/xmss_provider_test.cpp b/tests/unit/crypto/xmss_provider_test.cpp index d8ba77f..46ad7ea 100644 --- a/tests/unit/crypto/xmss_provider_test.cpp +++ b/tests/unit/crypto/xmss_provider_test.cpp @@ -10,22 +10,32 @@ using namespace lean::crypto::xmss; +uint64_t activation_epoch = 0; +uint64_t num_active_epochs = 10; +uint32_t epoch = 5; +uint32_t wrong_epoch = 6; + +XmssMessage message{0x42}; +XmssMessage wrong_message{0x43}; + class XmssProviderTest : public ::testing::Test { protected: - void SetUp() override { + static void SetUpTestSuite() { provider_ = std::make_unique(); + keypair = provider_->generateKeypair(activation_epoch, num_active_epochs); + keypair2 = provider_->generateKeypair(activation_epoch, num_active_epochs); } - std::unique_ptr provider_; + static std::unique_ptr provider_; + static XmssKeypair keypair; + static XmssKeypair keypair2; }; -TEST_F(XmssProviderTest, GenerateKeypair) { - uint64_t activation_epoch = 0; - uint64_t num_active_epochs = 10000; - - auto keypair = - provider_->generateKeypair(activation_epoch, num_active_epochs); +decltype(XmssProviderTest::provider_) XmssProviderTest::provider_; +decltype(XmssProviderTest::keypair) XmssProviderTest::keypair; +decltype(XmssProviderTest::keypair2) XmssProviderTest::keypair2; +TEST_F(XmssProviderTest, GenerateKeypair) { EXPECT_FALSE(keypair.public_key.empty()); EXPECT_NE(keypair.private_key, nullptr); @@ -34,13 +44,6 @@ TEST_F(XmssProviderTest, GenerateKeypair) { } TEST_F(XmssProviderTest, SignAndVerify) { - // Generate keypair - auto keypair = provider_->generateKeypair(0, 10000); - - // Create a test message (32 bytes as required by XMSS) - qtils::ByteVec message(32, 0x42); - uint32_t epoch = 100; - // Sign the message auto signature = provider_->sign(keypair.private_key, epoch, message); @@ -54,17 +57,9 @@ TEST_F(XmssProviderTest, SignAndVerify) { } TEST_F(XmssProviderTest, VerifyFailsWithWrongMessage) { - // Generate keypair - auto keypair = provider_->generateKeypair(0, 10000); - // Create and sign a message - qtils::ByteVec message(32, 0x42); - uint32_t epoch = 100; auto signature = provider_->sign(keypair.private_key, epoch, message); - // Modify the message - qtils::ByteVec wrong_message(32, 0x43); - // Verification should fail bool result = provider_->verify(keypair.public_key, wrong_message, epoch, signature); @@ -72,14 +67,8 @@ TEST_F(XmssProviderTest, VerifyFailsWithWrongMessage) { } TEST_F(XmssProviderTest, VerifyFailsWithWrongPublicKey) { - // Generate two keypairs - auto keypair1 = provider_->generateKeypair(0, 10000); - auto keypair2 = provider_->generateKeypair(0, 10000); - // Sign with first keypair - qtils::ByteVec message(32, 0x42); - uint32_t epoch = 100; - auto signature = provider_->sign(keypair1.private_key, epoch, message); + auto signature = provider_->sign(keypair.private_key, epoch, message); // Try to verify with second keypair's public key bool result = @@ -88,17 +77,26 @@ TEST_F(XmssProviderTest, VerifyFailsWithWrongPublicKey) { } TEST_F(XmssProviderTest, VerifyFailsWithWrongEpoch) { - // Generate keypair - auto keypair = provider_->generateKeypair(0, 10000); - // Create and sign a message with a specific epoch - qtils::ByteVec message(32, 0x42); - uint32_t signing_epoch = 100; - auto signature = provider_->sign(keypair.private_key, signing_epoch, message); + auto signature = provider_->sign(keypair.private_key, epoch, message); // Attempt to verify using a different epoch - uint32_t wrong_epoch = 101; bool result = provider_->verify(keypair.public_key, message, wrong_epoch, signature); EXPECT_FALSE(result); } + +TEST_F(XmssProviderTest, AggregateSignatures) { + std::vector public_keys{ + keypair.public_key, + keypair2.public_key, + }; + std::vector signatures{ + provider_->sign(keypair.private_key, epoch, message), + provider_->sign(keypair2.private_key, epoch, message), + }; + auto aggregated_signature = + provider_->aggregateSignatures(public_keys, signatures, epoch, message); + EXPECT_TRUE(provider_->verifyAggregatedSignatures( + public_keys, epoch, message, aggregated_signature)); +} diff --git a/vcpkg-overlay/qdrvm-crates/portfile.cmake b/vcpkg-overlay/qdrvm-crates/portfile.cmake index a41b129..e49d09b 100644 --- a/vcpkg-overlay/qdrvm-crates/portfile.cmake +++ b/vcpkg-overlay/qdrvm-crates/portfile.cmake @@ -2,8 +2,8 @@ vcpkg_check_linkage(ONLY_STATIC_LIBRARY) vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO qdrvm/qdrvm-crates - REF refs/tags/v1.0.6 - SHA512 bede49cb4d42ada9daefee57b8b2ba7726231fcfbdfff28ed8c0f91fdb97cb56bfdc4baef611943454424668bdc9bc2fd9e284426934e19bb822452c10cf7be9 + REF 43196257e153b55d290435e609ef129186aa8904 + SHA512 0a92b2e1f5e2d35c5166f19d20f42033d92d9ef6da6742436b2470d37125cc9fd9cc74c4d7fe1a7213f94c56bef6da6a0f37addc21b4c32fa6b2b8126e0db642 ) vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}" OPTIONS "-DQDRVM_BIND_CRATES=c_hash_sig"