iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

commit f9d343a9c69d52d122879d9a2e7dd6dde03bc007
parent b517bea55ea4967dc84ae0ca56697be5d67704f5
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Sun,  2 Aug 2026 20:40:39 +0200

Normalize fallback VDF retarget samples

Diffstat:
Msrc/domain.rs | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 135 insertions(+), 3 deletions(-)

diff --git a/src/domain.rs b/src/domain.rs @@ -2358,9 +2358,10 @@ impl Ledger { .filter(|pair| pair[0].height > 0) .take(VDF_RETARGET_WINDOW_BLOCKS) { - total_observed_ms += u128::from(clamped_vdf_retarget_observed_block_ms( - pair[1].timestamp_ms - pair[0].timestamp_ms, - )); + let Some(observed_ms) = vdf_retarget_observed_block_ms(&pair[0], &pair[1]) else { + continue; + }; + total_observed_ms += u128::from(observed_ms); observed_blocks += 1; } if observed_blocks == 0 { @@ -3980,6 +3981,16 @@ fn clamped_vdf_retarget_observed_block_ms(observed_block_ms: u64) -> u64 { ) } +fn vdf_retarget_observed_block_ms(parent: &Block, child: &Block) -> Option<u64> { + if child.finalizer_mode == FinalizerMode::Recovery { + return None; + } + + let rank_multiplier = u64::from(child.finalizer_rank) + 1; + let rank_adjusted_ms = (child.timestamp_ms - parent.timestamp_ms) / rank_multiplier; + Some(clamped_vdf_retarget_observed_block_ms(rank_adjusted_ms)) +} + fn unix_now_ms() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -4109,6 +4120,27 @@ mod tests { block } + fn vdf_retarget_sample_block( + timestamp_ms: u64, + finalizer_mode: FinalizerMode, + finalizer_rank: u32, + ) -> Block { + Block { + height: 1, + prev_hash: String::new(), + timestamp_ms, + miner: String::new(), + finalizer_mode, + finalizer_rank, + reward: 0, + vdf_rounds: 1, + vdf_output: String::new(), + leader_proof: None, + transactions: Vec::new(), + hash: String::new(), + } + } + const TEST_BURN_AMOUNT: Amount = MICRO_IUNA / 10; fn unsigned_mine(ledger: &Ledger, recipient: &str) -> Transaction { @@ -4494,6 +4526,36 @@ mod tests { } #[test] + fn vdf_retarget_observed_block_time_scales_ticket_fallback_rank() { + let parent = vdf_retarget_sample_block(0, FinalizerMode::Ticket, 0); + let rank_one_child = + vdf_retarget_sample_block(VDF_TARGET_BLOCK_MS * 2, FinalizerMode::Ticket, 1); + let rank_two_child = + vdf_retarget_sample_block(VDF_TARGET_BLOCK_MS * 3, FinalizerMode::Ticket, 2); + + assert_eq!( + vdf_retarget_observed_block_ms(&parent, &rank_one_child), + Some(VDF_TARGET_BLOCK_MS) + ); + assert_eq!( + vdf_retarget_observed_block_ms(&parent, &rank_two_child), + Some(VDF_TARGET_BLOCK_MS) + ); + } + + #[test] + fn vdf_retarget_observed_block_time_ignores_recovery_blocks() { + let parent = vdf_retarget_sample_block(0, FinalizerMode::Ticket, 0); + let recovery_child = + vdf_retarget_sample_block(RECOVERY_BLOCK_DELAY_MS, FinalizerMode::Recovery, 0); + + assert_eq!( + vdf_retarget_observed_block_ms(&parent, &recovery_child), + None + ); + } + + #[test] fn vdf_retarget_keeps_rounds_inside_deadband() { let current = 1_000; let low_deadband_edge = @@ -4548,6 +4610,76 @@ mod tests { } #[test] + fn fallback_block_retarget_uses_rank_adjusted_observed_time() { + let alice = Wallet::from_seed("fallback-retarget-alice"); + let bob = Wallet::from_seed("fallback-retarget-bob"); + let wallets = [&alice, &bob]; + let mut genesis = BTreeMap::new(); + genesis.insert(alice.address().to_string(), 1_000); + genesis.insert(bob.address().to_string(), 1_000); + let mut ledger = Ledger::new_with_genesis_burns( + genesis, + vec![ + GenesisBurn::new(alice.address(), 1), + GenesisBurn::new(bob.address(), 1), + ], + 100, + ) + .unwrap(); + + let primary = ledger.expected_leader_for_next_block().unwrap(); + let primary_wallet = wallets + .into_iter() + .find(|wallet| wallet.address() == primary) + .unwrap(); + apply_preverified_burn_block_at(&mut ledger, primary_wallet, VDF_TARGET_BLOCK_MS); + assert_eq!(ledger.vdf_rounds(), 100); + + let primary = ledger.expected_leader_for_next_block().unwrap(); + let fallback = wallets + .into_iter() + .find(|wallet| wallet.address() != primary) + .unwrap(); + let block = apply_preverified_burn_block_at( + &mut ledger, + fallback, + VDF_TARGET_BLOCK_MS + VDF_TARGET_BLOCK_MS * 2, + ); + + assert_eq!(block.finalizer_rank, 1); + assert_eq!(block.vdf_rounds, 200); + assert_eq!(ledger.vdf_rounds(), 100); + } + + #[test] + fn recovery_block_is_excluded_from_vdf_retarget_observations() { + let alice = Wallet::from_seed("recovery-retarget-alice"); + let bob = Wallet::from_seed("recovery-retarget-bob"); + let mut genesis = BTreeMap::new(); + genesis.insert(alice.address().to_string(), 1_000); + genesis.insert(bob.address().to_string(), 1_000); + let mut ledger = Ledger::new_with_genesis_burns( + genesis, + vec![GenesisBurn::new(alice.address(), 1)], + 100, + ) + .unwrap(); + + apply_preverified_burn_block_at(&mut ledger, &alice, VDF_TARGET_BLOCK_MS); + assert_eq!(ledger.vdf_rounds(), 100); + + let burn = ledger.build_burn(&bob, 1, 0).unwrap(); + ledger.submit_transaction(burn).unwrap(); + let block = ledger + .mine_recovery_block(&bob, VDF_TARGET_BLOCK_MS + RECOVERY_BLOCK_DELAY_MS) + .unwrap(); + assert_eq!(block.finalizer_mode, FinalizerMode::Recovery); + ledger.apply_block(block).unwrap(); + + assert_eq!(ledger.vdf_rounds(), 100); + } + + #[test] fn generated_vdf_retarget_increases_after_fast_blocks_above_legacy_limit() { let legacy_limit = u64::from(u32::MAX); let fast_observed_ms = [