iuna

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

commit 94cb4e3a98893e05c380cdcb0a74e82bdc6d89fb
parent fd542e99811a874d0662b81d1b1cb93a26e5a769
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Thu,  6 Aug 2026 10:46:56 +0200

Prepare v0.2.27

Diffstat:
MCargo.lock | 2+-
MCargo.toml | 2+-
Msrc/app.rs | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/domain.rs | 21++++++++++++++++++---
4 files changed, 174 insertions(+), 5 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -537,7 +537,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "iuna" -version = "0.2.26" +version = "0.2.27" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iuna" -version = "0.2.26" +version = "0.2.27" edition = "2024" license = "Apache-2.0" diff --git a/src/app.rs b/src/app.rs @@ -1186,6 +1186,7 @@ impl NodeCore { fn wallet_build_ledger(&self) -> Result<Ledger> { let mut ledger = self.ledger.clone(); + self.reserve_local_block_anchor_inputs(&mut ledger)?; for (commitment, payload) in &self.owned_blinded_payloads { if self.ledger.has_unrevealed_blinded_transaction(commitment) && !ledger.has_transaction(payload.signature()) @@ -1196,6 +1197,16 @@ impl NodeCore { Ok(ledger) } + fn reserve_local_block_anchor_inputs(&self, ledger: &mut Ledger) -> Result<()> { + let Some((height, burn)) = &self.local_block_anchor_burn else { + return Ok(()); + }; + if *height == ledger.height() && !ledger.has_transaction(burn.signature()) { + ledger.reserve_transaction_inputs(burn)?; + } + Ok(()) + } + pub fn mine_one(&mut self) -> Result<Block> { self.mine_one_at(now_ms()) } @@ -3103,6 +3114,149 @@ mod tests { assert!(outbox.is_empty()); assert!(node.prepare_automatic_finalization(1).work.is_some()); } + + #[test] + fn wallet_building_reserves_local_anchor_burn_inputs() { + let alice = Wallet::from_seed("local-anchor-reserve-alice"); + let bob = Wallet::from_seed("local-anchor-reserve-bob"); + let finalizers = [alice.clone(), bob.clone()]; + let mut allocations = BTreeMap::new(); + allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA); + allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA); + let ledger = Ledger::new_with_genesis_burns( + allocations, + finalizers + .iter() + .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA)) + .collect(), + 1, + ) + .unwrap(); + let leader = ledger.expected_leader_for_next_block().unwrap(); + let leader_wallet = finalizers + .iter() + .find(|wallet| wallet.address() == leader) + .unwrap() + .clone(); + let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled( + leader_wallet, + ledger, + true, + MICRO_IUNA / 10, + 1, + ); + + let plan = node.prepare_automatic_finalization(1); + assert!(plan.burned.is_some()); + let (_, anchor_burn) = node + .local_block_anchor_burn + .clone() + .expect("leader burn should be held as a local block anchor"); + let Transaction::Burn { inputs, .. } = anchor_burn else { + panic!("local block anchor must be a burn"); + }; + let anchor_inputs = inputs + .iter() + .map(|input| input.outpoint.clone()) + .collect::<Vec<_>>(); + + let blinded = node + .blinded_burn_with_fee(MICRO_IUNA / 10, 1, node.chain_height() + 4) + .unwrap(); + + assert!( + blinded + .inputs + .iter() + .all(|input| !anchor_inputs.contains(&input.outpoint)), + "blinded wallet transactions must not spend inputs reserved by the local anchor burn" + ); + + let work = node.prepare_next_block_with_local_anchor(2).unwrap(); + let vdf_output = run_vdf(work.vdf_seed(), work.vdf_rounds()); + let mut peer_ledger = node.clone_ledger(); + let block = node + .complete_prepared_block_at(work, vdf_output, VDF_TARGET_BLOCK_MS) + .unwrap(); + assert!(block.transactions.iter().any(Transaction::is_burn)); + assert!( + block + .blinded_transactions + .iter() + .any(|transaction| transaction.commitment == blinded.commitment) + ); + peer_ledger.apply_block_at(block, u64::MAX).unwrap(); + assert_eq!( + node.ledger().status().tip_hash, + peer_ledger.status().tip_hash + ); + } + + #[test] + fn locally_produced_blocks_import_on_independent_peer_ledger() { + let alice = Wallet::from_seed("producer-parity-alice"); + let bob = Wallet::from_seed("producer-parity-bob"); + let carol = Wallet::from_seed("producer-parity-carol"); + let wallets = [alice.clone(), bob.clone(), carol.clone()]; + let mut allocations = BTreeMap::new(); + for wallet in &wallets { + allocations.insert(wallet.address().to_string(), 20 * MICRO_IUNA); + } + let genesis_burns = wallets + .iter() + .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA)) + .collect(); + let mut producer_ledger = + Ledger::new_with_genesis_burns(allocations, genesis_burns, 1).unwrap(); + let mut peer_ledger = producer_ledger.clone(); + + for step in 0..8 { + assert_eq!( + producer_ledger.status().tip_hash, + peer_ledger.status().tip_hash + ); + let leader = producer_ledger + .expected_leader_for_next_block() + .expect("test chain should have an eligible leader"); + let leader_wallet = wallet_for_address(&wallets, &leader).clone(); + let timestamp_ms = (step + 1) as u64 * VDF_TARGET_BLOCK_MS; + let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled( + leader_wallet.clone(), + producer_ledger.clone(), + true, + MICRO_IUNA / 10, + 1, + ); + let plan = node.prepare_automatic_finalization(timestamp_ms); + assert!(plan.burned.is_some()); + + match step % 3 { + 0 => { + let _ = node.blinded_burn_with_fee(1, 0, node.chain_height() + 4); + } + 1 => { + let recipient = wallets[(step + 1) % wallets.len()].address(); + let _ = + node.blinded_transfer_with_fee(recipient, 1, 0, node.chain_height() + 4); + } + _ => {} + } + + let work = node + .prepare_next_block_with_local_anchor(timestamp_ms) + .unwrap(); + let vdf_output = run_vdf(work.vdf_seed(), work.vdf_rounds()); + let block = node + .complete_prepared_block_at(work, vdf_output, timestamp_ms) + .unwrap(); + peer_ledger.apply_block_at(block, u64::MAX).unwrap(); + producer_ledger = node.clone_ledger(); + assert_eq!( + producer_ledger.status().tip_hash, + peer_ledger.status().tip_hash + ); + } + } } #[derive(Debug, Default)] diff --git a/src/domain.rs b/src/domain.rs @@ -2848,6 +2848,14 @@ impl Ledger { Ok(self.submit_transaction_with_outcome(transaction)?.added()) } + pub(crate) fn reserve_transaction_inputs(&mut self, transaction: &Transaction) -> Result<()> { + self.validate_new_transaction(transaction)?; + let mut utxos = self.utxos.clone(); + spend_inputs(transaction, &mut utxos)?; + self.utxos = utxos; + Ok(()) + } + pub fn submit_blinded_transaction(&mut self, transaction: BlindedTransaction) -> Result<bool> { if self.has_blinded_transaction(&transaction.commitment) { return Ok(false); @@ -3046,11 +3054,18 @@ impl Ledger { } pub fn apply_locally_mined_block(&mut self, block: Block) -> Result<()> { - self.apply_preverified_block(block) + self.apply_self_produced_block_at(block, unix_now_ms()) + } + + pub(crate) fn apply_self_produced_block_at(&mut self, block: Block, now_ms: u64) -> Result<()> { + self.verify_self_produced_block_at(&block, now_ms)?; + self.apply_preverified_block_at(block, now_ms) } - pub(crate) fn apply_preverified_block(&mut self, block: Block) -> Result<()> { - self.apply_preverified_block_at(block, unix_now_ms()) + pub(crate) fn verify_self_produced_block_at(&self, block: &Block, now_ms: u64) -> Result<()> { + let mut verifier = self.clone(); + verifier.apply_preverified_block_at(block.clone(), now_ms)?; + Ok(()) } pub(crate) fn apply_preverified_block_at(&mut self, block: Block, now_ms: u64) -> Result<()> {