iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

commit de3f62279a3e571d8cd1a89d90d4ad3050919d26
parent cae387c48f6f2caec2757a2e2cd7d25f3cf8d203
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Tue, 15 Sep 2026 08:42:15 +0200

test(protocol): run regular block-budget coverage post-activation

Diffstat:
Msrc/domain/adversarial_tests.rs | 50++++++++++++++++++++++++++++++++++++++------------
Msrc/domain/ledger_v2.rs | 133++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 170 insertions(+), 13 deletions(-)

diff --git a/src/domain/adversarial_tests.rs b/src/domain/adversarial_tests.rs @@ -14,9 +14,9 @@ use super::{ BurnBundleSignature, BurnCommitteeMember, BurnLeaderRank, ChainSnapshot, FinalizerMode, GRINDING_RESISTANCE_ACTIVATION_HEIGHT, GenesisBurn, LeaderProofPayload, Ledger, MAX_BLOCK_BYTES, MAX_BURN_BUNDLE_BYTES, MICRO_IUNA, MaskedBurn, - OBJECTIVE_FINALITY_ACTIVATION_HEIGHT, OutPoint, Transaction, TransactionSubmitOutcome, - TxOutput, UtxoLineageRoot, VDF_TARGET_BLOCK_MS, Wallet, genesis_allocation_outpoint, hex_hash, - reward_outputs_for_block, run_vdf, + OBJECTIVE_FINALITY_ACTIVATION_HEIGHT, OutPoint, TRANSACTION_V2_ACTIVATION_HEIGHT, Transaction, + TransactionSubmitOutcome, TxOutput, UtxoLineageRoot, VDF_TARGET_BLOCK_MS, Wallet, + genesis_allocation_outpoint, hex_hash, reward_outputs_for_block, run_vdf, }; const NOW_MS: u64 = 10_000_000_000; @@ -214,6 +214,14 @@ impl Harness { .unwrap_or_else(|| panic!("seed {} missing wallet {address}", self.seed)) } + fn set_next_height(&mut self, next_height: u64) { + self.ledger.chain.last_mut().unwrap().height = next_height.saturating_sub(1); + for ticket in &mut self.ledger.tickets { + ticket.eligible_from_height = next_height; + ticket.eligible_until_height = next_height; + } + } + fn next_rank(&self, rank: usize) -> BurnLeaderRank { let height = self.ledger.height() + 1; let ticket = self @@ -4341,9 +4349,14 @@ fn attack_economics_committee_capture_requires_matured_lineage_weight() { #[test] fn performance_budget_block_validation_rejects_count_and_byte_overflow() { let mut harness = harness_for_percent(1_500, 25); + harness.set_next_height(TRANSACTION_V2_ACTIVATION_HEIGHT.unwrap().saturating_add(1)); let leader = harness.next_rank(0); let finalizer = harness.wallet(&leader.owner).clone(); - harness.submit_anchor_burn(&finalizer); + let anchor = harness + .ledger + .build_burn_for_next_block(&finalizer, 1, 1) + .unwrap(); + harness.ledger.submit_transaction(anchor).unwrap(); let block = harness.finish_ticket_block_from_pending(0, Vec::new()); let now_ms = NOW_MS.saturating_add(block.timestamp_ms); @@ -4466,11 +4479,14 @@ fn blockspace_flood_stays_bounded_by_transaction_count_and_bytes() { let senders = (0..(max_test_transactions + 32)) .map(|index| Wallet::from_seed(&format!("blockspace-flood-sender-{index}"))) .collect::<Vec<_>>(); + let v2_migrators = (0..32) + .map(|index| Wallet::from_seed(&format!("blockspace-flood-v2-migrator-{index}"))) + .collect::<Vec<_>>(); let mut allocations = BTreeMap::new(); allocations.insert(finalizer.address().to_string(), 10 * MICRO_IUNA); allocations.insert(recipient.address().to_string(), 10 * MICRO_IUNA); - for sender in &senders { - allocations.insert(sender.address().to_string(), 10 * MICRO_IUNA); + for wallet in senders.iter().chain(&v2_migrators) { + allocations.insert(wallet.address().to_string(), 10 * MICRO_IUNA); } let mut ledger = Ledger::new_with_genesis_burns( allocations, @@ -4478,6 +4494,12 @@ fn blockspace_flood_stays_bounded_by_transaction_count_and_bytes() { 1, ) .unwrap(); + let next_height = TRANSACTION_V2_ACTIVATION_HEIGHT.unwrap().saturating_add(1); + ledger.chain.last_mut().unwrap().height = next_height.saturating_sub(1); + for ticket in &mut ledger.tickets { + ticket.eligible_from_height = next_height; + ticket.eligible_until_height = next_height; + } ledger.launch_profile.max_block_transactions = max_test_transactions; for sender in &senders { @@ -4486,7 +4508,11 @@ fn blockspace_flood_stays_bounded_by_transaction_count_and_bytes() { .unwrap(); ledger.submit_transaction(tx).unwrap(); } - let anchor = ledger.build_burn(&finalizer, 1, 1).unwrap(); + for migrator in &v2_migrators { + let transaction = ledger.build_v2_migration(migrator, 10).unwrap(); + ledger.submit_transaction_v2(transaction).unwrap(); + } + let anchor = ledger.build_burn_for_next_block(&finalizer, 1, 1).unwrap(); ledger.submit_transaction(anchor.clone()).unwrap(); let prepared = ledger @@ -4502,11 +4528,11 @@ fn blockspace_flood_stays_bounded_by_transaction_count_and_bytes() { .any(|tx| tx.signature() == anchor.signature()), "flooded block did not preserve the required finalizer anchor burn" ); + let selected_transactions = block.transactions.len() + block.transactions_v2.len(); + assert!(!block.transactions_v2.is_empty()); assert!( - block.transactions.len() <= max_test_transactions, - "block selected too many transactions: {} > {}", - block.transactions.len(), - max_test_transactions + selected_transactions <= max_test_transactions, + "block selected too many transactions: {selected_transactions} > {max_test_transactions}" ); let block_bytes = ledger.consensus_block_size_bytes(&block).unwrap(); assert!( @@ -4518,7 +4544,7 @@ fn blockspace_flood_stays_bounded_by_transaction_count_and_bytes() { .apply_block_at(block, NOW_MS.saturating_add(VDF_TARGET_BLOCK_MS)) .unwrap(); assert!( - !ledger.pending().is_empty(), + !ledger.pending().is_empty() || !ledger.pending_v2().is_empty(), "blockspace flood should leave excess paid transactions pending instead of exceeding limits" ); } diff --git a/src/domain/ledger_v2.rs b/src/domain/ledger_v2.rs @@ -454,7 +454,22 @@ mod tests { use std::collections::BTreeMap; use super::*; - use crate::domain::{GenesisBurn, LaunchProfile, SignatureScheme, TransactionV2Output, Wallet}; + use crate::domain::{ + GenesisBurn, LaunchProfile, SignatureScheme, TRANSACTION_V2_ACTIVATION_HEIGHT, + TransactionV2Output, Wallet, + }; + + fn set_next_height(ledger: &mut Ledger, next_height: u64) { + ledger.chain.last_mut().unwrap().height = next_height.saturating_sub(1); + for ticket in &mut ledger.tickets { + ticket.eligible_from_height = next_height; + ticket.eligible_until_height = next_height; + } + } + + fn post_activation_height() -> u64 { + TRANSACTION_V2_ACTIVATION_HEIGHT.unwrap().saturating_add(1) + } #[test] fn migration_validation_switches_at_3000_and_creates_a_hybrid_utxo() { @@ -821,4 +836,120 @@ mod tests { ); assert!(ledger.pending_v2().is_empty()); } + + #[test] + fn normal_post_activation_block_flow_shares_the_transaction_count_budget() { + let finalizer = Wallet::from_seed("v2-post-activation-count-finalizer"); + let recipient = Wallet::from_seed("v2-post-activation-count-recipient"); + let migrators = (0..4) + .map(|index| Wallet::from_seed(&format!("v2-post-activation-count-{index}"))) + .collect::<Vec<_>>(); + let legacy_senders = (0..2) + .map(|index| Wallet::from_seed(&format!("legacy-post-activation-count-{index}"))) + .collect::<Vec<_>>(); + let mut allocations = BTreeMap::from([ + (finalizer.address().to_string(), 100), + (recipient.address().to_string(), 100), + ]); + for wallet in migrators.iter().chain(&legacy_senders) { + allocations.insert(wallet.address().to_string(), 100); + } + let mut ledger = Ledger::new_with_genesis_burns_and_profile( + allocations, + vec![GenesisBurn::new(finalizer.address(), 10)], + 1, + LaunchProfile::local_testnet(), + ) + .unwrap(); + let next_height = post_activation_height(); + set_next_height(&mut ledger, next_height); + ledger.launch_profile.max_block_transactions = 4; + + let anchor = ledger.build_burn_for_next_block(&finalizer, 1, 1).unwrap(); + ledger.submit_transaction(anchor.clone()).unwrap(); + for (index, wallet) in migrators.iter().enumerate() { + let migration = ledger + .build_v2_migration(wallet, 10 + index as u64) + .unwrap(); + ledger.submit_transaction_v2(migration).unwrap(); + } + for wallet in &legacy_senders { + let transfer = ledger + .build_transfer(wallet, recipient.address(), 10, 1) + .unwrap(); + ledger.submit_transaction(transfer).unwrap(); + } + + let prepared = ledger + .prepare_next_block( + finalizer.address(), + ledger.tip().timestamp_ms.saturating_add(1), + ) + .unwrap(); + assert_eq!( + prepared.transactions.len() + prepared.transactions_v2.len(), + ledger.launch_profile.max_block_transactions + ); + assert!( + prepared + .transactions + .iter() + .any(|transaction| transaction.signature() == anchor.signature()) + ); + assert!(!prepared.transactions_v2.is_empty()); + let block = prepared.finish(&finalizer, "preverified-vdf".to_string()); + ledger.apply_preverified_block_at(block, u64::MAX).unwrap(); + + assert_eq!(ledger.height(), next_height); + assert!( + ledger.pending().len() + ledger.pending_v2().len() > 0, + "transactions over the shared block limit must remain pending" + ); + } + + #[test] + fn normal_post_activation_v2_block_enforces_the_exact_byte_boundary() { + let finalizer = Wallet::from_seed("v2-post-activation-bytes-finalizer"); + let migrator = Wallet::from_seed("v2-post-activation-bytes-migrator"); + let mut ledger = Ledger::new_with_genesis_burns_and_profile( + BTreeMap::from([ + (finalizer.address().to_string(), 100), + (migrator.address().to_string(), 100), + ]), + vec![GenesisBurn::new(finalizer.address(), 10)], + 1, + LaunchProfile::local_testnet(), + ) + .unwrap(); + let next_height = post_activation_height(); + set_next_height(&mut ledger, next_height); + let anchor = ledger.build_burn_for_next_block(&finalizer, 1, 1).unwrap(); + ledger.submit_transaction(anchor).unwrap(); + let migration = ledger.build_v2_migration(&migrator, 3).unwrap(); + ledger.submit_transaction_v2(migration).unwrap(); + let prepared = ledger + .prepare_next_block( + finalizer.address(), + ledger.tip().timestamp_ms.saturating_add(1), + ) + .unwrap(); + assert_eq!(prepared.transactions_v2.len(), 1); + let block = prepared.finish(&finalizer, "preverified-vdf".to_string()); + let block_bytes = ledger.consensus_block_size_bytes(&block).unwrap(); + + let mut exact = ledger.clone(); + exact.launch_profile.max_block_bytes = block_bytes; + exact + .apply_preverified_block_at(block.clone(), u64::MAX) + .expect("post-activation v2 block at the byte limit should validate"); + + let mut one_byte_over = ledger; + one_byte_over.launch_profile.max_block_bytes = block_bytes.saturating_sub(1); + assert!( + one_byte_over + .apply_preverified_block_at(block, u64::MAX) + .is_err(), + "post-activation v2 block one byte over the limit validated" + ); + } }