iuna

iuna

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

commit 5cb23d0d5bde68864e41bc6836e4f3631d68452a
parent 1d21300512bdd7ad9dffa9f3572cb302efc1ac79
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Wed, 19 Aug 2026 20:59:43 +0200

Add candidate promotion rehearsal

Diffstat:
MPLAN.md | 4++++
Msrc/app.rs | 13+++++++++----
Msrc/main_tests.rs | 133++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 145 insertions(+), 5 deletions(-)

diff --git a/PLAN.md b/PLAN.md @@ -148,6 +148,10 @@ Acceptance: ## 7. Candidate Promotion Rehearsal +Status: started with explicit candidate/mainnet peer-network IDs and a restart +rehearsal that preserves the candidate genesis, tip, launch profile, UTXOs, +ticket ranks, and transfer history before mining the next promoted block. + Goal: prove candidate-to-mainnet promotion preserves coins. Scenario: diff --git a/src/app.rs b/src/app.rs @@ -39,7 +39,9 @@ pub type SharedPeerBook = Arc<Mutex<PeerBook>>; pub const DEFAULT_BURN_PER_BLOCK: Amount = 0; pub const DEFAULT_VDF_ROUNDS: u32 = 67_000_000; pub const PROTOCOL_VERSION: u32 = 1; -pub const NETWORK_ID: &str = "iuna-mainnet-candidate-v1"; +pub const MAINNET_CANDIDATE_NETWORK_ID: &str = "iuna-mainnet-candidate-v1"; +pub const MAINNET_NETWORK_ID: &str = "iuna-mainnet-v1"; +pub const NETWORK_ID: &str = MAINNET_CANDIDATE_NETWORK_ID; pub const BLOCK_REQUEST_LIMIT: usize = 128; pub const TRANSACTION_BATCH_LIMIT: usize = 128; const IMPORT_REBROADCAST_LIMIT: usize = 128; @@ -57,15 +59,18 @@ static DEBUG_LOGGING: AtomicBool = AtomicBool::new(false); #[cfg(test)] mod tests { use super::{ - BLOCK_REQUEST_LIMIT, DEFAULT_VDF_ROUNDS, NETWORK_ID, PROTOCOL_VERSION, - TRANSACTION_BATCH_LIMIT, + BLOCK_REQUEST_LIMIT, DEFAULT_VDF_ROUNDS, MAINNET_CANDIDATE_NETWORK_ID, MAINNET_NETWORK_ID, + NETWORK_ID, PROTOCOL_VERSION, TRANSACTION_BATCH_LIMIT, }; #[test] fn mainnet_candidate_network_parameters_are_frozen() { assert_eq!(DEFAULT_VDF_ROUNDS, 67_000_000); assert_eq!(PROTOCOL_VERSION, 1); - assert_eq!(NETWORK_ID, "iuna-mainnet-candidate-v1"); + assert_eq!(MAINNET_CANDIDATE_NETWORK_ID, "iuna-mainnet-candidate-v1"); + assert_eq!(MAINNET_NETWORK_ID, "iuna-mainnet-v1"); + assert_ne!(MAINNET_CANDIDATE_NETWORK_ID, MAINNET_NETWORK_ID); + assert_eq!(NETWORK_ID, MAINNET_CANDIDATE_NETWORK_ID); assert_eq!(BLOCK_REQUEST_LIMIT, 128); assert_eq!(TRANSACTION_BATCH_LIMIT, 128); } diff --git a/src/main_tests.rs b/src/main_tests.rs @@ -5,7 +5,7 @@ use iuna::{ chain_store::SqliteChainStore, config_store::UiConfig, ui_data_store::SqliteUiDataStore, wallet_store, }, - app::{DEFAULT_BURN_PER_BLOCK, NodeCore}, + app::{DEFAULT_BURN_PER_BLOCK, MAINNET_CANDIDATE_NETWORK_ID, MAINNET_NETWORK_ID, NodeCore}, domain::{BLOCK_REWARD, GenesisBurn, Ledger, MICRO_IUNA, VDF_TARGET_BLOCK_MS, Wallet}, }; use rusqlite::Connection; @@ -48,6 +48,67 @@ fn ledger_with_one_mined_block(wallet: &Wallet) -> Ledger { ledger } +fn promoted_candidate_ledger() -> (Ledger, Vec<Wallet>, String) { + let wallets = [ + Wallet::from_seed("promotion-candidate-alice"), + Wallet::from_seed("promotion-candidate-bob"), + Wallet::from_seed("promotion-candidate-carol"), + ]; + let mut allocations = BTreeMap::new(); + for wallet in &wallets { + allocations.insert(wallet.address().to_string(), 20 * MICRO_IUNA); + } + let mut ledger = Ledger::new_with_genesis_burns( + allocations, + wallets + .iter() + .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA)) + .collect(), + 1, + ) + .unwrap(); + let finalizer_owner = ledger + .burn_leader_ranks_for_block(ledger.height() + 1) + .unwrap() + .first() + .unwrap() + .owner + .clone(); + let finalizer = wallets + .iter() + .find(|wallet| wallet.address() == finalizer_owner) + .unwrap(); + let sender = wallets + .iter() + .find(|wallet| wallet.address() != finalizer.address()) + .unwrap(); + let recipient = wallets + .iter() + .find(|wallet| { + wallet.address() != finalizer.address() && wallet.address() != sender.address() + }) + .unwrap(); + let transfer = ledger + .build_transfer(sender, recipient.address(), MICRO_IUNA, MICRO_IUNA) + .unwrap(); + let transfer_signature = transfer.signature().to_string(); + ledger.submit_transaction(transfer).unwrap(); + let anchor = ledger + .build_burn(finalizer, MICRO_IUNA, MICRO_IUNA) + .unwrap(); + ledger.submit_transaction(anchor).unwrap(); + let block = ledger + .mine_next_block(finalizer, VDF_TARGET_BLOCK_MS) + .unwrap(); + ledger.apply_locally_mined_block(block).unwrap(); + assert!( + ledger.has_transaction(&transfer_signature), + "candidate rehearsal chain should contain a transfer before promotion" + ); + + (ledger, wallets.to_vec(), transfer_signature) +} + #[test] fn encrypted_startup_wallet_loads_as_locked_metadata() { let dir = tempdir().unwrap(); @@ -545,6 +606,76 @@ async fn startup_resumes_persisted_chain_without_genesis_flag() { } #[tokio::test] +async fn candidate_promotion_reuses_chain_data_and_can_continue_mining() { + assert_eq!(MAINNET_CANDIDATE_NETWORK_ID, "iuna-mainnet-candidate-v1"); + assert_eq!(MAINNET_NETWORK_ID, "iuna-mainnet-v1"); + assert_ne!(MAINNET_CANDIDATE_NETWORK_ID, MAINNET_NETWORK_ID); + let dir = tempdir().unwrap(); + let chain_path = dir.path().join("chain.sqlite3"); + let store = SqliteChainStore::open(&chain_path).unwrap(); + let (candidate, wallets, transfer_signature) = promoted_candidate_ledger(); + let candidate_genesis = candidate.genesis_hash().to_string(); + let candidate_tip = candidate.tip_hash().to_string(); + let candidate_profile = candidate.launch_profile().clone(); + let candidate_utxos = candidate.all_utxos(); + let candidate_next_ranks = candidate + .burn_leader_ranks_for_block(candidate.height() + 1) + .unwrap(); + store.save(&candidate.snapshot()).unwrap(); + let opts = parse(&["--chain-db", chain_path.to_str().unwrap()]) + .unwrap() + .unwrap(); + + let mut promoted = initialize_ledger(&opts, wallets[0].address(), &store, opts.p2p_addr) + .await + .unwrap(); + + assert_eq!(promoted.genesis_hash(), candidate_genesis); + assert_eq!(promoted.tip_hash(), candidate_tip); + assert_eq!(promoted.launch_profile(), &candidate_profile); + assert_eq!(promoted.all_utxos(), candidate_utxos); + assert_eq!( + promoted + .burn_leader_ranks_for_block(promoted.height() + 1) + .unwrap(), + candidate_next_ranks + ); + assert!(promoted.has_transaction(&transfer_signature)); + + let next_owner = promoted + .burn_leader_ranks_for_block(promoted.height() + 1) + .unwrap() + .first() + .unwrap() + .owner + .clone(); + let next_finalizer = wallets + .iter() + .find(|wallet| wallet.address() == next_owner) + .unwrap(); + let anchor = promoted + .build_burn(next_finalizer, MICRO_IUNA, MICRO_IUNA) + .unwrap(); + promoted.submit_transaction(anchor).unwrap(); + let next_block = promoted + .mine_next_block( + next_finalizer, + promoted + .chain() + .last() + .unwrap() + .timestamp_ms + .saturating_add(VDF_TARGET_BLOCK_MS), + ) + .unwrap(); + promoted.apply_locally_mined_block(next_block).unwrap(); + + assert_eq!(promoted.genesis_hash(), candidate_genesis); + assert_eq!(promoted.height(), candidate.height() + 1); + assert_ne!(promoted.tip_hash(), candidate_tip); +} + +#[tokio::test] async fn startup_resumes_persisted_chain_with_network_accepted_future_tip() { let dir = tempdir().unwrap(); let chain_path = dir.path().join("chain.sqlite3");