commit fd3e2deb0be0c264730adc8cb638525cec4fafc1
parent 6490b49513e4c7eb97efb7dfa7d5fd920a4d956f
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Mon, 7 Sep 2026 09:28:21 +0200
Start fallback VDFs before rank slots
Diffstat:
3 files changed, 65 insertions(+), 8 deletions(-)
diff --git a/src/app/automatic_mining.rs b/src/app/automatic_mining.rs
@@ -776,6 +776,39 @@ mod tests {
}
#[test]
+ fn configured_top_percentage_limits_fallback_ticket_vdfs() {
+ let wallets = (0..4)
+ .map(|index| Wallet::from_seed(&format!("fallback-percent-wallet-{index}")))
+ .collect::<Vec<_>>();
+ let ledger = funded_ledger(&wallets);
+ assert_eq!(ledger.finalizer_rank_count_for_next_block(), wallets.len());
+
+ let rank_one = wallets
+ .iter()
+ .find(|wallet| ledger.finalizer_rank_for_next_block(wallet.address()) == Some(1))
+ .expect("fixture should contain the rank-one fallback")
+ .clone();
+ let mut node =
+ NodeCore::from_ledger_with_burn_fee_and_enabled(rank_one, ledger, true, 0, 1);
+
+ node.set_recovery_vdf_top_rank_percent(25);
+ assert!(!node.wallet_rank_runs_vdf(1));
+ let excluded = node.prepare_automatic_finalization(1);
+ assert!(excluded.work.is_none());
+ assert_eq!(
+ excluded.skipped_reason.as_deref(),
+ Some("wallet finalizer rank 1 is outside the top 25% VDF threshold")
+ );
+
+ node.set_recovery_vdf_top_rank_percent(50);
+ assert!(node.wallet_rank_runs_vdf(1));
+
+ node.set_recovery_vdf_top_rank_percent(0);
+ assert!(node.wallet_rank_runs_vdf(0));
+ assert!(!node.wallet_rank_runs_vdf(1));
+ }
+
+ #[test]
fn same_slot_bundles_for_different_members_do_not_conflict_locally() {
let wallet = Wallet::from_seed("rank-bundle-cache-node");
let ledger = funded_ledger(std::slice::from_ref(&wallet));
diff --git a/src/main.rs b/src/main.rs
@@ -730,9 +730,10 @@ async fn run_automatic_finalizer(node: SharedNode, gossip: p2p::GossipNetwork, d
let seed = work.vdf_seed().to_string();
let rounds = work.vdf_rounds();
let publish_at_ms = work.timestamp_ms();
+ let precheck_at_ms = automatic_finalization_precheck_time(now_ms(), publish_at_ms);
let precheck = {
let node = node.lock().await;
- node.precheck_prepared_block_without_vdf_at(&work, now_ms())
+ node.precheck_prepared_block_without_vdf_at(&work, precheck_at_ms)
};
if let Err(error) = precheck {
let message = format!("skipped before VDF: {error:#}");
@@ -867,6 +868,14 @@ async fn run_automatic_finalizer(node: SharedNode, gossip: p2p::GossipNetwork, d
}
}
+fn automatic_finalization_precheck_time(now_ms: u64, publish_at_ms: u64) -> u64 {
+ // A fallback ticket's rank slot can be well beyond the normal future-drift
+ // allowance. Validate the candidate as it will stand when that known slot
+ // opens, so its VDF can run in advance; final application still validates
+ // the actual publication timestamp against the then-current clock.
+ now_ms.max(publish_at_ms)
+}
+
fn should_log_automatic_finalization_skip(
last_logged_skip: &mut Option<(u64, String)>,
height: u64,
diff --git a/src/main_tests.rs b/src/main_tests.rs
@@ -19,13 +19,13 @@ use super::{
GENESIS_INITIAL_BURN_FEE, GENESIS_INITIAL_BURN_PER_BLOCK, StartupWallet,
apply_cli_p2p_config_overrides, apply_cli_stratum_config_overrides,
apply_startup_mining_config_overrides, apply_startup_setup_config_override,
- apply_startup_wallet_password_config, configured_p2p_announce_addr, configured_p2p_bind_addr,
- configured_stratum_addr, extrapolate_vdf_rounds, help_text, initial_burn_fee,
- initial_burn_per_block, initialize_ledger, load_startup_wallet, measure_vdf_rounds,
- parse_startup_bool_env_value, parse_startup_pow_mining_workers_env_value,
- persist_chain_snapshot, project_ui_data_store, run_chain_persistence_with_interval,
- setup_ledger, should_defer_sync_checkpoint, should_log_automatic_finalization_skip,
- validate_wallet_for_mode,
+ apply_startup_wallet_password_config, automatic_finalization_precheck_time,
+ configured_p2p_announce_addr, configured_p2p_bind_addr, configured_stratum_addr,
+ extrapolate_vdf_rounds, help_text, initial_burn_fee, initial_burn_per_block, initialize_ledger,
+ load_startup_wallet, measure_vdf_rounds, parse_startup_bool_env_value,
+ parse_startup_pow_mining_workers_env_value, persist_chain_snapshot, project_ui_data_store,
+ run_chain_persistence_with_interval, setup_ledger, should_defer_sync_checkpoint,
+ should_log_automatic_finalization_skip, validate_wallet_for_mode,
};
fn parse(args: &[&str]) -> anyhow::Result<Option<CliOptions>> {
@@ -555,6 +555,21 @@ fn automatic_finalization_skip_logging_only_reports_state_changes() {
}
#[test]
+fn automatic_finalization_prechecks_future_fallback_at_its_publish_time() {
+ let now_ms = 1_000;
+ let publish_at_ms = now_ms + 20 * 60 * 1_000;
+
+ assert_eq!(
+ automatic_finalization_precheck_time(now_ms, publish_at_ms),
+ publish_at_ms
+ );
+ assert_eq!(
+ automatic_finalization_precheck_time(publish_at_ms, now_ms),
+ publish_at_ms
+ );
+}
+
+#[test]
fn removed_wallet_seed_is_rejected() {
let error = parse(&["--wallet-seed", "alice", "--genesis"]).unwrap_err();
assert!(error.to_string().contains("--wallet-seed was removed"));