iuna

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

commit 86041b1cd9746a7e0d7bad73c5f44c3633118db8
parent 94cb4e3a98893e05c380cdcb0a74e82bdc6d89fb
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Thu,  6 Aug 2026 12:12:23 +0200

Prepare v0.2.28

Diffstat:
MCargo.lock | 2+-
MCargo.toml | 2+-
Msrc/adapters/http.rs | 28+++++++++++++++++++++++++---
Msrc/app.rs | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/domain.rs | 41+++++++++++++++++++++++++++++++++++++++++
5 files changed, 183 insertions(+), 7 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -537,7 +537,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "iuna" -version = "0.2.27" +version = "0.2.28" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iuna" -version = "0.2.27" +version = "0.2.28" edition = "2024" license = "Apache-2.0" diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -4207,9 +4207,9 @@ mod tests { }, app::{GossipEnvelope, NodeCore, PeerBook, PeerDirection, PeerInfo, StratumStatus}, domain::{ - Amount, BlindedReveal, BlindedTransaction, Block, ChainSnapshot, LaunchProfile, Ledger, - MICRO_IUNA, MINE_FINALIZER_FEE, MaskedBlindedReveal, OutPoint, RevealBundleSection, - RevealBundleSignature, Transaction, TxInput, TxOutput, Wallet, + Amount, BlindedReveal, BlindedTransaction, Block, ChainSnapshot, GenesisBurn, + LaunchProfile, Ledger, MICRO_IUNA, MINE_FINALIZER_FEE, MaskedBlindedReveal, OutPoint, + RevealBundleSection, RevealBundleSignature, Transaction, TxInput, TxOutput, Wallet, }, }; @@ -5340,6 +5340,28 @@ mod tests { assert!(!selectable.iter().any(|row| row.outpoint == spent_outpoint)); } + #[test] + fn wallet_utxo_rows_keep_local_anchor_spends_visible_as_pending() { + let alice = Wallet::from_seed("wallet-utxo-local-anchor-alice"); + let mut allocations = BTreeMap::new(); + allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA); + let ledger = Ledger::new_with_genesis_burns( + allocations, + vec![GenesisBurn::new(alice.address(), MICRO_IUNA)], + 1, + ) + .unwrap(); + let mut node = + NodeCore::from_ledger_with_burn_fee_and_enabled(alice.clone(), ledger, true, 1, 0); + + let plan = node.prepare_automatic_finalization(1); + assert!(plan.burned.is_some()); + let rows = wallet_utxo_rows(&node.wallet_view_ledger().unwrap(), alice.address()); + + assert!(!rows.is_empty()); + assert!(rows.iter().any(|row| !row.spendable)); + } + fn fake_block(height: u64, transactions: Vec<Transaction>) -> Block { Block { height, diff --git a/src/app.rs b/src/app.rs @@ -388,7 +388,10 @@ impl NodeCore { } pub fn wallet_view_ledger(&self) -> Result<Ledger> { - self.wallet_build_ledger() + let mut ledger = self.ledger.clone(); + self.queue_local_block_anchor(&mut ledger)?; + self.queue_owned_blinded_payloads(&mut ledger)?; + Ok(ledger) } pub fn chain(&self) -> &[Block] { @@ -682,6 +685,17 @@ impl NodeCore { } } + if let Some((height, burn)) = &self.local_block_anchor_burn { + if *height == self.ledger.height() && !self.ledger.has_transaction(burn.signature()) { + let output_total = transaction_output_total_for_address(burn, address); + let input_total = + transaction_input_total_from_outputs(burn, address, &confirmed_outputs); + balance = balance + .saturating_sub(input_total) + .saturating_add(output_total); + } + } + balance } @@ -906,12 +920,28 @@ impl NodeCore { } pub fn receive_blinded_transaction(&mut self, tx: BlindedTransaction) -> Result<()> { + if self.blinded_transaction_conflicts_with_local_anchor(&tx) { + return Ok(()); + } if self.ledger.submit_blinded_transaction(tx.clone())? { self.outbox.push(GossipEnvelope::BlindedTransaction(tx)); } Ok(()) } + fn blinded_transaction_conflicts_with_local_anchor(&self, tx: &BlindedTransaction) -> bool { + let Some((height, burn)) = &self.local_block_anchor_burn else { + return false; + }; + if *height != self.ledger.height() || self.ledger.has_transaction(burn.signature()) { + return false; + } + let anchor_inputs = transaction_input_outpoints(burn); + tx.inputs + .iter() + .any(|input| anchor_inputs.contains(&input.outpoint)) + } + pub fn receive_blinded_reveal(&mut self, reveal: BlindedReveal) -> Result<()> { self.receive_blinded_reveal_without_bundle_publish(reveal)?; Ok(()) @@ -1187,6 +1217,11 @@ impl NodeCore { fn wallet_build_ledger(&self) -> Result<Ledger> { let mut ledger = self.ledger.clone(); self.reserve_local_block_anchor_inputs(&mut ledger)?; + self.queue_owned_blinded_payloads(&mut ledger)?; + Ok(ledger) + } + + fn queue_owned_blinded_payloads(&self, ledger: &mut Ledger) -> Result<()> { for (commitment, payload) in &self.owned_blinded_payloads { if self.ledger.has_unrevealed_blinded_transaction(commitment) && !ledger.has_transaction(payload.signature()) @@ -1194,7 +1229,17 @@ impl NodeCore { let _ = ledger.submit_transaction(payload.clone())?; } } - Ok(ledger) + Ok(()) + } + + fn queue_local_block_anchor(&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()) { + let _ = ledger.submit_transaction(burn.clone())?; + } + Ok(()) } fn reserve_local_block_anchor_inputs(&self, ledger: &mut Ledger) -> Result<()> { @@ -2228,6 +2273,16 @@ fn transaction_input_total_from_outputs( .fold(0_u64, |total, amount| total.saturating_add(*amount)) } +fn transaction_input_outpoints(transaction: &Transaction) -> BTreeSet<OutPoint> { + match transaction { + Transaction::Transfer { inputs, .. } | Transaction::Burn { inputs, .. } => inputs, + Transaction::Mine { .. } => return BTreeSet::new(), + } + .iter() + .map(|input| input.outpoint.clone()) + .collect() +} + #[cfg(test)] mod tests { use std::collections::BTreeMap; @@ -3148,6 +3203,7 @@ mod tests { let plan = node.prepare_automatic_finalization(1); assert!(plan.burned.is_some()); + assert!(node.status().wallet_balance < node.ledger().balance_of(node.wallet_address())); let (_, anchor_burn) = node .local_block_anchor_burn .clone() @@ -3193,6 +3249,63 @@ mod tests { } #[test] + fn inbound_blinded_transaction_conflicting_with_local_anchor_is_not_queued() { + let alice = Wallet::from_seed("local-anchor-inbound-alice"); + let bob = Wallet::from_seed("local-anchor-inbound-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.clone(), + 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 anchor_inputs = super::transaction_input_outpoints(&anchor_burn) + .into_iter() + .collect::<Vec<_>>(); + let conflicting_payload = node + .ledger() + .build_transfer_with_inputs(&leader_wallet, bob.address(), 1, 0, &anchor_inputs) + .unwrap(); + let conflicting = node + .ledger() + .build_blinded_transaction(&leader_wallet, conflicting_payload, node.chain_height() + 4) + .unwrap(); + + node.receive_blinded_transaction(conflicting.transaction) + .unwrap(); + + assert!(node.ledger().pending_blinded_transactions().is_empty()); + assert!(node.drain_outbox().is_empty()); + assert!(node.prepare_automatic_finalization(1).work.is_some()); + } + + #[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"); diff --git a/src/domain.rs b/src/domain.rs @@ -1071,6 +1071,16 @@ fn transaction_inputs_available( .all(|input| utxos.contains_key(&input.outpoint)) } +fn blinded_transaction_inputs_available( + transaction: &BlindedTransaction, + utxos: &BTreeMap<OutPoint, TxOutput>, +) -> bool { + transaction + .inputs + .iter() + .all(|input| utxos.contains_key(&input.outpoint)) +} + #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Block { pub height: u64, @@ -3202,6 +3212,7 @@ impl Ledger { .filter(|transaction| { !included_blinded.contains(&transaction.commitment) && new_height < transaction.expires_at_height + && blinded_transaction_inputs_available(transaction, &available) && self.validate_blinded_transaction(transaction).is_ok() }) .collect(); @@ -7859,6 +7870,36 @@ mod tests { } #[test] + fn pending_blinded_transactions_with_spent_inputs_are_pruned_after_block_apply() { + let alice = Wallet::from_seed("pending-blind-spent-prune-alice"); + let mut mempool_ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); + let mut block_ledger = mempool_ledger.clone(); + let amount = mempool_ledger.balance_of(alice.address()); + let blinded = mempool_ledger + .build_blinded_burn(&alice, amount, 0, mempool_ledger.height() + 4) + .unwrap(); + mempool_ledger + .submit_blinded_transaction(blinded.transaction.clone()) + .unwrap(); + + let burn = block_ledger.build_burn(&alice, amount, 0).unwrap(); + let Transaction::Burn { inputs, .. } = &burn else { + panic!("expected burn transaction"); + }; + assert!(blinded.transaction.inputs.iter().any(|input| { + inputs + .iter() + .any(|burn_input| burn_input.outpoint == input.outpoint) + })); + block_ledger.submit_transaction(burn).unwrap(); + let block = block_ledger.mine_next_block(&alice, 1).unwrap(); + + mempool_ledger.apply_block(block).unwrap(); + + assert!(mempool_ledger.pending_blinded_transactions().is_empty()); + } + + #[test] fn block_selection_can_include_multiple_mine_actions() { let alice = Wallet::from_seed("mine-multiple-actions-alice"); let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA);