diff --git a/src/common/network.cpp b/src/common/network.cpp index 4ca49b1..3418ab2 100644 --- a/src/common/network.cpp +++ b/src/common/network.cpp @@ -56,6 +56,7 @@ const NetworkConfig kMainnet{ .confidential_utxo_activation_height = std::numeric_limits::max(), .deferred_exit_activation_height = 10017, .bootstrap_penalty_exit_protection_activation_height = 10145, + .empty_active_set_epoch_escape_activation_height = 10145, .default_seeds = {}, .economics_policies = { @@ -106,6 +107,10 @@ bool bootstrap_penalty_exit_protection_active_at_height(const NetworkConfig& net return height >= network.bootstrap_penalty_exit_protection_activation_height; } +bool empty_active_set_epoch_escape_active_at_height(const NetworkConfig& network, std::uint64_t height) { + return height >= network.empty_active_set_epoch_escape_activation_height; +} + bool onboarding_admission_pow_enabled(const NetworkConfig& network) { return network.onboarding_admission_pow_difficulty_bits != 0; } diff --git a/src/common/network.hpp b/src/common/network.hpp index e3d36cd..0a77ef2 100644 --- a/src/common/network.hpp +++ b/src/common/network.hpp @@ -60,6 +60,7 @@ struct NetworkConfig { std::uint64_t confidential_utxo_activation_height{std::numeric_limits::max()}; std::uint64_t deferred_exit_activation_height{std::numeric_limits::max()}; std::uint64_t bootstrap_penalty_exit_protection_activation_height{std::numeric_limits::max()}; + std::uint64_t empty_active_set_epoch_escape_activation_height{std::numeric_limits::max()}; std::vector default_seeds; std::vector economics_policies; }; @@ -72,6 +73,7 @@ bool finality_binding_active_at_height(const NetworkConfig& network, std::uint64 bool availability_recovery_active_at_height(const NetworkConfig& network, std::uint64_t height); bool confidential_utxo_active_at_height(const NetworkConfig& network, std::uint64_t height); bool bootstrap_penalty_exit_protection_active_at_height(const NetworkConfig& network, std::uint64_t height); +bool empty_active_set_epoch_escape_active_at_height(const NetworkConfig& network, std::uint64_t height); bool onboarding_admission_pow_enabled(const NetworkConfig& network); bool validator_join_admission_pow_enabled(const NetworkConfig& network); diff --git a/src/consensus/canonical_derivation.cpp b/src/consensus/canonical_derivation.cpp index 5755cbe..f7391e8 100644 --- a/src/consensus/canonical_derivation.cpp +++ b/src/consensus/canonical_derivation.cpp @@ -2288,6 +2288,16 @@ bool canonical_checkpoints_equal(const storage::FinalizedCommitteeCheckpoint& a, a.ordered_ticket_hashes == b.ordered_ticket_hashes && a.ordered_ticket_nonces == b.ordered_ticket_nonces; } +std::optional empty_active_set_escape_member(const ValidatorRegistry& validators) { + std::optional out; + for (const auto& [pub, info] : validators.all()) { + if (!info.has_bond || info.bonded_amount == 0) continue; + if (info.status == ValidatorStatus::BANNED || info.status == ValidatorStatus::ONBOARDING) continue; + if (!out.has_value() || pub < *out) out = pub; + } + return out; +} + bool derive_next_epoch_checkpoint_from_state(const CanonicalDerivationConfig& cfg, const CanonicalDerivedState& state, std::uint64_t epoch_start_height, storage::FinalizedCommitteeCheckpoint* out, std::string* error) { @@ -2347,6 +2357,25 @@ bool derive_next_epoch_checkpoint_from_state(const CanonicalDerivationConfig& cf checkpoint.ordered_ticket_hashes.push_back(candidate.ticket_work_hash); checkpoint.ordered_ticket_nonces.push_back(candidate.ticket_nonce); } + if (checkpoint.ordered_members.empty() && active_validator_count == 0 && + empty_active_set_epoch_escape_active_at_height(cfg.network, epoch_start_height)) { + if (auto fallback = empty_active_set_escape_member(state.validators); fallback.has_value()) { + auto info = state.validators.get(*fallback); + if (!info.has_value()) { + if (error) *error = "escape-fallback-validator-missing-info"; + return false; + } + checkpoint.derivation_mode = storage::FinalizedCommitteeDerivationMode::FALLBACK; + checkpoint.fallback_reason = storage::FinalizedCommitteeFallbackReason::INSUFFICIENT_ELIGIBLE_OPERATORS; + checkpoint.ordered_members = {*fallback}; + checkpoint.ordered_operator_ids = {canonical_operator_id(*fallback, *info)}; + checkpoint.ordered_base_weights = {info->bonded_amount}; + checkpoint.ordered_ticket_bonus_bps = {0}; + checkpoint.ordered_final_weights = {info->bonded_amount}; + checkpoint.ordered_ticket_hashes = {zero_hash()}; + checkpoint.ordered_ticket_nonces = {0}; + } + } *out = std::move(checkpoint); return true; } diff --git a/src/node/node.cpp b/src/node/node.cpp index 7ee5e12..56d17e3 100644 --- a/src/node/node.cpp +++ b/src/node/node.cpp @@ -1691,6 +1691,16 @@ storage::FinalizedCommitteeCheckpoint build_finalized_committee_checkpoint_from_ return checkpoint; } +std::optional empty_active_set_escape_member_runtime(const consensus::ValidatorRegistry& validators) { + std::optional out; + for (const auto& [pub, info] : validators.all()) { + if (!info.has_bond || info.bonded_amount == 0) continue; + if (info.status == consensus::ValidatorStatus::BANNED || info.status == consensus::ValidatorStatus::ONBOARDING) continue; + if (!out.has_value() || pub < *out) out = pub; + } + return out; +} + bool finalized_checkpoint_matches_epoch_snapshot(const storage::FinalizedCommitteeCheckpoint& checkpoint, const consensus::EpochCommitteeSnapshot& snapshot) { if (checkpoint.ordered_members != snapshot.ordered_members) return false; @@ -4629,11 +4639,31 @@ storage::FinalizedCommitteeCheckpoint Node::build_finalized_committee_checkpoint previous_checkpoint_for_epoch(finalized_committee_checkpoints_, epoch_start_height, cfg_.network.committee_epoch_blocks); const auto decision = decide_availability_committee_mode(&availability_state_, cfg_.availability, previous_checkpoint, validators_, epoch_start_height); - return build_finalized_committee_checkpoint_from_candidates( + auto checkpoint = build_finalized_committee_checkpoint_from_candidates( epoch_start_height, consensus::committee_epoch_seed(epoch_randomness, epoch_start_height), ticket_difficulty_bits_for_epoch_locked(epoch_start_height, active_validator_count), active, cfg_.max_committee, decision.mode, decision.fallback_reason, decision.eligible_operator_count, decision.min_eligible_operators, decision.effective_committee_size, decision.adaptive); + if (checkpoint.ordered_members.empty() && active_validator_count == 0 && + empty_active_set_epoch_escape_active_at_height(cfg_.network, epoch_start_height)) { + if (auto fallback = empty_active_set_escape_member_runtime(validators_); fallback.has_value()) { + auto info = validators_.get(*fallback); + if (info.has_value()) { + checkpoint.derivation_mode = storage::FinalizedCommitteeDerivationMode::FALLBACK; + checkpoint.fallback_reason = storage::FinalizedCommitteeFallbackReason::INSUFFICIENT_ELIGIBLE_OPERATORS; + checkpoint.ordered_members = {*fallback}; + checkpoint.ordered_operator_ids = {consensus::canonical_operator_id(*fallback, *info)}; + checkpoint.ordered_base_weights = {info->bonded_amount}; + checkpoint.ordered_ticket_bonus_bps = {0}; + checkpoint.ordered_final_weights = {info->bonded_amount}; + checkpoint.ordered_ticket_hashes = {zero_hash()}; + checkpoint.ordered_ticket_nonces = {0}; + log_line("epoch-empty-active-escape epoch=" + std::to_string(epoch_start_height) + + " selected=" + short_pub_hex(*fallback) + " reason=deterministic-fallback-member"); + } + } + } + return checkpoint; } void Node::persist_finalized_committee_checkpoint_locked(std::uint64_t epoch_start_height,