iuna

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

commit 8d3e2db98b1091017586d748a64d5b915c4f0405
parent 673f95ad3ef80ca0ddf176bee50d54ab6816bd61
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Thu, 30 Jul 2026 15:15:07 +0200

Allow VDF rounds above legacy limit

Diffstat:
Msrc/adapters/chain_store.rs | 44++++++++++++++++++++++++++++++++++++++++++--
Msrc/adapters/http.rs | 4++--
Msrc/app.rs | 4++--
Msrc/domain.rs | 157++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/main.rs | 23++++++++++++-----------
5 files changed, 186 insertions(+), 46 deletions(-)

diff --git a/src/adapters/chain_store.rs b/src/adapters/chain_store.rs @@ -57,7 +57,7 @@ pub struct BlockMetricRow { pub total_burned_amount: Amount, pub fees_amount: Amount, pub reward_amount: Amount, - pub vdf_rounds: u32, + pub vdf_rounds: u64, pub finalizer_rank: u32, } @@ -421,7 +421,7 @@ mod tests { use crate::domain::{BLOCK_REWARD, GenesisBurn, Ledger, Wallet}; - use super::SqliteChainStore; + use super::{BlockMetricRow, SqliteChainStore, replace_metrics}; #[test] fn sqlite_chain_store_roundtrips_snapshot() { @@ -491,6 +491,46 @@ mod tests { } #[test] + fn sqlite_chain_store_roundtrips_vdf_round_metrics_above_legacy_u32_limit() { + let dir = tempdir().unwrap(); + let store = SqliteChainStore::open(dir.path().join("chain.sqlite3")).unwrap(); + let vdf_rounds = u64::from(u32::MAX) + 42; + + store + .with_connection_mut(|connection| { + let transaction = connection.transaction().unwrap(); + replace_metrics( + &transaction, + &[BlockMetricRow { + height: 1, + block_hash: "hash".to_string(), + timestamp_ms: 1_000, + block_time_ms: Some(415_000), + mine_difficulty_bits: 12, + circulating_supply: 100, + transaction_count: 0, + transfer_count: 0, + burn_count: 0, + mine_count: 0, + burned_amount: 0, + total_burned_amount: 0, + fees_amount: 0, + reward_amount: 0, + vdf_rounds, + finalizer_rank: 0, + }], + )?; + transaction.commit().unwrap(); + Ok(()) + }) + .unwrap(); + + let metrics = store.load_metrics().unwrap(); + assert_eq!(metrics.len(), 1); + assert_eq!(metrics[0].vdf_rounds, vdf_rounds); + } + + #[test] fn sqlite_chain_store_metrics_include_genesis_reward_when_burn_consumes_allocation() { let dir = tempdir().unwrap(); let store = SqliteChainStore::open(dir.path().join("chain.sqlite3")).unwrap(); diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -314,7 +314,7 @@ struct UiBlock { finalizer_rank: u32, reward: Amount, total_fees: Amount, - vdf_rounds: u32, + vdf_rounds: u64, vdf_output: String, leader_proof: Option<crate::domain::LeaderProof>, transactions: Vec<UiTransaction>, @@ -4237,7 +4237,7 @@ mod tests { fn metric_row( height: u64, block_time_ms: Option<u64>, - vdf_rounds: u32, + vdf_rounds: u64, ) -> crate::adapters::chain_store::BlockMetricRow { crate::adapters::chain_store::BlockMetricRow { height, diff --git a/src/app.rs b/src/app.rs @@ -48,7 +48,7 @@ pub fn debug_logging_enabled() -> bool { pub struct NodeConfig { pub wallet: Wallet, pub genesis_allocations: BTreeMap<String, Amount>, - pub vdf_rounds: u32, + pub vdf_rounds: u64, pub burn_per_block: Amount, pub burn_fee: Amount, } @@ -198,7 +198,7 @@ pub struct MiningStatus { pub automatic_pow_mine_fee: Amount, pub last_auto_pow_mine_anchor: Option<String>, pub last_auto_pow_mine_status: Option<String>, - pub vdf_rounds: u32, + pub vdf_rounds: u64, pub vdf_target_block_ms: u64, pub current_leader: Option<String>, pub wallet_is_current_leader: bool, diff --git a/src/domain.rs b/src/domain.rs @@ -18,6 +18,7 @@ pub const DEFAULT_TRANSACTION_FEE: Amount = MICRO_IUNA; pub const DEFAULT_FEE_PER_BYTE: Amount = 1; pub const MAX_BLOCK_BYTES: usize = 100_000; pub const VDF_TARGET_BLOCK_MS: u64 = 10 * 60 * 1_000; +pub const MAX_VDF_ROUNDS: u64 = i64::MAX as u64; pub const MINE_DIFFICULTY_BITS: u32 = 12; const MINE_RETARGET_WINDOW_BLOCKS: u64 = 10; const MINE_TARGET_ACTIONS_PER_BLOCK: u64 = 1; @@ -30,7 +31,7 @@ const MAX_ORPHAN_TRANSACTIONS: usize = 1_024; const MAX_BLOCK_TRANSACTIONS: usize = 1_000; const DEFAULT_TICKET_MATURITY_DELAY: u64 = 3; const DEFAULT_TICKET_EXPIRY_WINDOW: u64 = 3; -const MIN_VDF_ROUNDS: u32 = 1; +const MIN_VDF_ROUNDS: u64 = 1; const VDF_RETARGET_WINDOW_BLOCKS: usize = 10; const MAX_VDF_RETARGET_STEP_PERCENT: u128 = 10; const MIN_VDF_RETARGET_OBSERVED_BLOCK_MS: u64 = VDF_TARGET_BLOCK_MS / 4; @@ -767,7 +768,7 @@ pub struct Block { #[serde(default)] pub finalizer_rank: u32, pub reward: Amount, - pub vdf_rounds: u32, + pub vdf_rounds: u64, pub vdf_output: String, pub leader_proof: Option<LeaderProof>, pub transactions: Vec<Transaction>, @@ -946,7 +947,7 @@ pub struct PreparedBlock { miner: String, finalizer_rank: u32, reward: Amount, - vdf_rounds: u32, + vdf_rounds: u64, vdf_seed: String, leader_ticket: BurnTicket, transactions: Vec<Transaction>, @@ -957,7 +958,7 @@ impl PreparedBlock { &self.vdf_seed } - pub fn vdf_rounds(&self) -> u32 { + pub fn vdf_rounds(&self) -> u64 { self.vdf_rounds } @@ -998,7 +999,7 @@ struct BlockDraft { miner: String, finalizer_rank: u32, reward: Amount, - vdf_rounds: u32, + vdf_rounds: u64, vdf_output: String, leader_proof: Option<LeaderProof>, transactions: Vec<Transaction>, @@ -1019,7 +1020,7 @@ pub struct ChainStatus { #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct ChainSnapshot { pub genesis_allocations: BTreeMap<String, Amount>, - pub vdf_rounds: u32, + pub vdf_rounds: u64, pub launch_profile: LaunchProfile, pub blocks: Vec<Block>, } @@ -1175,13 +1176,13 @@ pub struct Ledger { pending: Vec<Transaction>, orphans: Vec<Transaction>, mine_reward: Amount, - initial_vdf_rounds: u32, - vdf_rounds: u32, + initial_vdf_rounds: u64, + vdf_rounds: u64, launch_profile: LaunchProfile, } impl Ledger { - pub fn new(genesis_allocations: BTreeMap<String, Amount>, vdf_rounds: u32) -> Self { + pub fn new(genesis_allocations: BTreeMap<String, Amount>, vdf_rounds: u64) -> Self { Self::new_with_genesis_transactions(genesis_allocations, Vec::new(), vdf_rounds) .expect("empty genesis transactions are valid") } @@ -1189,7 +1190,7 @@ impl Ledger { pub fn new_with_genesis_burns( genesis_allocations: BTreeMap<String, Amount>, genesis_burns: Vec<GenesisBurn>, - vdf_rounds: u32, + vdf_rounds: u64, ) -> Result<Self> { let transactions = genesis_burns .into_iter() @@ -1207,7 +1208,7 @@ impl Ledger { fn new_with_genesis_transactions( genesis_allocations: BTreeMap<String, Amount>, genesis_transactions: Vec<Transaction>, - vdf_rounds: u32, + vdf_rounds: u64, ) -> Result<Self> { validate_genesis_allocations(&genesis_allocations)?; let launch_profile = LaunchProfile::default(); @@ -1570,7 +1571,7 @@ impl Ledger { self.transaction_by_signature(signature).is_some() } - pub fn vdf_rounds(&self) -> u32 { + pub fn vdf_rounds(&self) -> u64 { self.vdf_rounds } @@ -2128,7 +2129,7 @@ impl Ledger { timestamps[timestamps.len() / 2] } - fn next_vdf_rounds_after_tip(&self) -> u32 { + fn next_vdf_rounds_after_tip(&self) -> u64 { let Some(tip) = self.chain.last() else { return self.vdf_rounds; }; @@ -2425,7 +2426,7 @@ impl Ledger { }) } - fn vdf_rounds_for_finalizer_rank(&self, rank: u32) -> Result<u32> { + fn vdf_rounds_for_finalizer_rank(&self, rank: u32) -> Result<u64> { vdf_rounds_for_finalizer_rank(self.vdf_rounds, rank) } @@ -2519,14 +2520,20 @@ fn weighted_ticket_draw(parent: &Block, target_height: u64, rank: u32, total_wei u128::from_be_bytes(bytes) % total_weight } -fn vdf_rounds_for_finalizer_rank(base_rounds: u32, rank: u32) -> Result<u32> { - base_rounds - .checked_mul(rank.checked_add(1).context("finalizer rank overflows")?) - .context("finalizer rank VDF rounds overflow") +fn vdf_rounds_for_finalizer_rank(base_rounds: u64, rank: u32) -> Result<u64> { + let rounds = base_rounds + .checked_mul(u64::from( + rank.checked_add(1).context("finalizer rank overflows")?, + )) + .context("finalizer rank VDF rounds overflow")?; + if rounds > MAX_VDF_ROUNDS { + bail!("finalizer rank VDF rounds exceed maximum"); + } + Ok(rounds) } -fn base_vdf_rounds_for_finalizer_rank(vdf_rounds: u32, rank: u32) -> u32 { - vdf_rounds / rank.saturating_add(1).max(1) +fn base_vdf_rounds_for_finalizer_rank(vdf_rounds: u64, rank: u32) -> u64 { + vdf_rounds / u64::from(rank.saturating_add(1).max(1)) } fn tickets_created_by_block(block: &Block, profile: &LaunchProfile) -> Result<Vec<BurnTicket>> { @@ -2950,7 +2957,7 @@ fn estimated_block_size_bytes(transactions: &[Transaction]) -> Result<usize> { miner: "f".repeat(64), finalizer_rank: 0, reward: u64::MAX, - vdf_rounds: u32::MAX, + vdf_rounds: u64::MAX, vdf_output: "f".repeat(64), leader_proof: Some(LeaderProof { ticket_id: "f".repeat(64), @@ -3297,7 +3304,7 @@ fn genesis_reward( } } -pub fn run_vdf(seed: &str, rounds: u32) -> String { +pub fn run_vdf(seed: &str, rounds: u64) -> String { let x = vdf_seed_element(seed); let mut y = x; for _ in 0..rounds { @@ -3309,7 +3316,7 @@ pub fn run_vdf(seed: &str, rounds: u32) -> String { encode_vdf_solution(y, proof) } -pub fn verify_vdf(seed: &str, rounds: u32, solution: &str) -> bool { +pub fn verify_vdf(seed: &str, rounds: u64, solution: &str) -> bool { let Some((y, proof)) = decode_vdf_solution(solution) else { return false; }; @@ -3331,7 +3338,7 @@ fn vdf_seed_element(seed: &str) -> u128 { 2 + (u128::from_be_bytes(bytes) % (VDF_MODULUS - 3)) } -fn vdf_challenge_prime(seed: &str, rounds: u32, output: u128) -> u64 { +fn vdf_challenge_prime(seed: &str, rounds: u64, output: u128) -> u64 { let digest = Sha256::digest(format!("iuna-vdf-challenge:{seed}:{rounds}:{output:x}")); let mut bytes = [0_u8; 8]; bytes.copy_from_slice(&digest[..8]); @@ -3339,7 +3346,7 @@ fn vdf_challenge_prime(seed: &str, rounds: u32, output: u128) -> u64 { next_odd_prime(candidate | 1) } -fn vdf_proof(x: u128, rounds: u32, challenge: u64) -> u128 { +fn vdf_proof(x: u128, rounds: u64, challenge: u64) -> u128 { let mut proof = 1_u128; let mut remainder = 1_u64 % challenge; for _ in 0..rounds { @@ -3385,7 +3392,7 @@ fn mod_pow(mut base: u128, mut exponent: u128) -> u128 { result } -fn pow_mod_small(base: u64, exponent: u32, modulus: u64) -> u64 { +fn pow_mod_small(base: u64, exponent: u64, modulus: u64) -> u64 { let mut result = 1_u128; let mut base = u128::from(base % modulus); let mut exponent = exponent; @@ -3421,7 +3428,7 @@ fn is_odd_prime(candidate: u64) -> bool { true } -fn retarget_vdf_rounds(current_rounds: u32, observed_block_ms: u64) -> u32 { +fn retarget_vdf_rounds(current_rounds: u64, observed_block_ms: u64) -> u64 { let current = u128::from(current_rounds); let observed = u128::from(observed_block_ms.max(1)); let raw_adjusted = current * u128::from(VDF_TARGET_BLOCK_MS) / observed; @@ -3429,8 +3436,10 @@ fn retarget_vdf_rounds(current_rounds: u32, observed_block_ms: u64) -> u32 { let min_next = current .saturating_sub(max_step) .max(u128::from(MIN_VDF_ROUNDS)); - let max_next = current.saturating_add(max_step).min(u128::from(u32::MAX)); - raw_adjusted.clamp(min_next, max_next) as u32 + let max_next = current + .saturating_add(max_step) + .min(u128::from(MAX_VDF_ROUNDS)); + raw_adjusted.clamp(min_next, max_next) as u64 } fn clamped_vdf_retarget_observed_block_ms(observed_block_ms: u64) -> u64 { @@ -3552,6 +3561,23 @@ mod tests { ledger.apply_block(block).unwrap(); } + fn apply_preverified_burn_block_at( + ledger: &mut Ledger, + wallet: &Wallet, + timestamp_ms: u64, + ) -> Block { + let burn = ledger.build_burn(wallet, 1, 0).unwrap(); + ledger.submit_transaction(burn).unwrap(); + let work = ledger + .prepare_next_block(wallet.address(), timestamp_ms) + .unwrap(); + let block = work.finish(wallet, "preverified-vdf".to_string()); + ledger + .apply_preverified_block_at(block.clone(), u64::MAX) + .unwrap(); + block + } + fn mine_with_output_and_fee( ledger: &Ledger, recipient: &str, @@ -3842,6 +3868,79 @@ mod tests { } #[test] + fn vdf_rounds_retarget_above_legacy_u32_limit_after_fast_blocks() { + let wallet = Wallet::from_seed("vdf-rounds-above-u32"); + let initial_rounds = u64::from(u32::MAX); + let mut allocations = BTreeMap::new(); + allocations.insert(wallet.address().to_string(), 1_000); + let mut ledger = Ledger::new_with_genesis_burns( + allocations, + vec![GenesisBurn::new(wallet.address(), 1)], + initial_rounds, + ) + .unwrap(); + + let block1 = apply_preverified_burn_block_at(&mut ledger, &wallet, VDF_TARGET_BLOCK_MS); + assert_eq!(block1.vdf_rounds, initial_rounds); + assert_eq!(ledger.vdf_rounds(), initial_rounds); + + let block2 = + apply_preverified_burn_block_at(&mut ledger, &wallet, VDF_TARGET_BLOCK_MS + 415_000); + assert_eq!(block2.vdf_rounds, initial_rounds); + + assert!( + ledger.vdf_rounds() > initial_rounds, + "fast blocks should retarget above the legacy u32 VDF rounds ceiling" + ); + } + + #[test] + fn generated_vdf_retarget_increases_after_fast_blocks_above_legacy_limit() { + let legacy_limit = u64::from(u32::MAX); + let fast_observed_ms = [ + MIN_VDF_RETARGET_OBSERVED_BLOCK_MS, + 300_000, + 415_000, + VDF_TARGET_BLOCK_MS - 1, + ]; + + for seed in 0..16_u64 { + let wallet = Wallet::from_seed(&format!("generated-vdf-retarget-{seed}")); + let initial_rounds = legacy_limit + 1 + seed * 1_000_003; + let mut allocations = BTreeMap::new(); + allocations.insert(wallet.address().to_string(), 1_000); + let mut ledger = Ledger::new_with_genesis_burns( + allocations, + vec![GenesisBurn::new(wallet.address(), 1)], + initial_rounds, + ) + .unwrap(); + let observed_ms = fast_observed_ms[seed as usize % fast_observed_ms.len()]; + + apply_preverified_burn_block_at(&mut ledger, &wallet, VDF_TARGET_BLOCK_MS); + let second = apply_preverified_burn_block_at( + &mut ledger, + &wallet, + VDF_TARGET_BLOCK_MS + observed_ms, + ); + + assert_eq!(second.vdf_rounds, initial_rounds); + assert!( + ledger.vdf_rounds() > initial_rounds, + "seed {seed} with observed {observed_ms}ms should raise VDF rounds from {initial_rounds}, got {}", + ledger.vdf_rounds() + ); + + let burn = ledger.build_burn(&wallet, 1, 0).unwrap(); + ledger.submit_transaction(burn).unwrap(); + let next_work = ledger + .prepare_next_block(wallet.address(), VDF_TARGET_BLOCK_MS + observed_ms * 2) + .unwrap(); + assert_eq!(next_work.vdf_rounds(), ledger.vdf_rounds()); + } + } + + #[test] fn block_timestamp_future_check_uses_supplied_network_time() { let wallet = Wallet::from_seed("adjusted-time-domain"); let mut allocations = BTreeMap::new(); diff --git a/src/main.rs b/src/main.rs @@ -15,7 +15,8 @@ use iuna::{ now_ms, set_debug_logging, }, domain::{ - Amount, ChainSnapshot, GenesisBurn, Ledger, MICRO_IUNA, VDF_TARGET_BLOCK_MS, run_vdf, + Amount, ChainSnapshot, GenesisBurn, Ledger, MAX_VDF_ROUNDS, MICRO_IUNA, + VDF_TARGET_BLOCK_MS, run_vdf, }, }; use tokio::sync::Mutex; @@ -23,8 +24,8 @@ use tokio::sync::Mutex; const GENESIS_BOOTSTRAP_BURN_AMOUNT: Amount = MICRO_IUNA; const GENESIS_INITIAL_BURN_PER_BLOCK: Amount = GENESIS_BOOTSTRAP_BURN_AMOUNT; const GENESIS_INITIAL_BURN_FEE: Amount = 0; -const VDF_MEASUREMENT_INITIAL_ROUNDS: u32 = 1_000_000; -const VDF_MEASUREMENT_MAX_ROUNDS: u32 = 100_000_000; +const VDF_MEASUREMENT_INITIAL_ROUNDS: u64 = 1_000_000; +const VDF_MEASUREMENT_MAX_ROUNDS: u64 = 100_000_000; const VDF_MEASUREMENT_MIN_ELAPSED: Duration = Duration::from_millis(150); #[tokio::main] @@ -460,7 +461,7 @@ fn start_genesis_ledger(wallet_address: &str) -> Result<Ledger> { ) } -fn measure_initial_vdf_rounds() -> u32 { +fn measure_initial_vdf_rounds() -> u64 { let seed = "iuna-vdf-calibration"; let (measured_rounds, elapsed) = measure_vdf_rounds( seed, @@ -482,12 +483,12 @@ fn measure_initial_vdf_rounds() -> u32 { fn measure_vdf_rounds( seed: &str, - initial_rounds: u32, + initial_rounds: u64, min_elapsed: Duration, - max_rounds_per_attempt: u32, -) -> (u32, Duration) { + max_rounds_per_attempt: u64, +) -> (u64, Duration) { let mut rounds = initial_rounds.max(1).min(max_rounds_per_attempt.max(1)); - let mut measured_rounds = 0_u32; + let mut measured_rounds = 0_u64; let mut measured_elapsed = Duration::ZERO; loop { @@ -503,14 +504,14 @@ fn measure_vdf_rounds( } } -fn extrapolate_vdf_rounds(measured_rounds: u32, elapsed: Duration, target: Duration) -> u32 { +fn extrapolate_vdf_rounds(measured_rounds: u64, elapsed: Duration, target: Duration) -> u64 { let elapsed_ns = elapsed.as_nanos().max(1); let target_ns = target.as_nanos().max(1); let rounds = u128::from(measured_rounds) .saturating_mul(target_ns) .saturating_div(elapsed_ns) .max(1); - rounds.min(u128::from(u32::MAX)) as u32 + rounds.min(u128::from(MAX_VDF_ROUNDS)) as u64 } async fn join_chain_ledger(join_peers: &[String], advertised_addr: SocketAddr) -> Result<Ledger> { @@ -1025,7 +1026,7 @@ mod tests { Duration::from_secs(0), Duration::from_millis(VDF_TARGET_BLOCK_MS), ), - u32::MAX + 6_000_000_000_000_000 ); }