iuna

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

commit 04a13b097eb8f7cf39df34b5e52743b4768ab324
parent 1bb3f739123f3f1c0b38230fc029e2a6371f4212
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Wed,  5 Aug 2026 23:39:31 +0200

Prepare v0.2.25

Diffstat:
MCargo.lock | 2+-
MCargo.toml | 2+-
Msrc/adapters/http.rs | 10+++++++---
Msrc/app.rs | 58+++++++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 60 insertions(+), 12 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -537,7 +537,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "iuna" -version = "0.2.24" +version = "0.2.25" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iuna" -version = "0.2.24" +version = "0.2.25" edition = "2024" license = "Apache-2.0" diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -3252,6 +3252,8 @@ const INDEX_HTML: &str = r#"<!doctype html> .tx-section { display: grid; gap: 8px; } .tx-section + .tx-section { margin-top: 10px; padding-top: 10px; border-top: 1px solid #2f363c; } .tx-section-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; color: #f4f7f8; font-size: 13px; font-weight: 800; } + summary.tx-section-title { cursor: pointer; } + details.tx-section:not([open]) { gap: 0; } .tx-section-meta { color: #8e979e; font-size: 12px; font-weight: 600; } .tx-card, .mempool-item { position: relative; display: grid; gap: 6px; border: 1px solid #2f363c; border-radius: 8px; padding: 12px; background: #111316; cursor: pointer; text-align: left; } .tx-card .pill, .mempool-item .pill { position: absolute; top: 10px; right: 10px; } @@ -3747,8 +3749,8 @@ const INDEX_HTML: &str = r#"<!doctype html> <div class="muted" x-show="selectedBlock.transactions.length === 0">No envelope transactions</div> </div> <template x-for="bundle in selectedBlock.reveal_bundles || selectedBlock.revealBundles || []" :key="bundle.hash"> - <div class="tx-section"> - <div class="tx-section-title"><span x-text="`Reveal bundle ${bundle.slot}`"></span><span class="tx-section-meta"><span x-text="short(bundle.member)"></span> · <span x-text="bundle.byte_size || bundle.byteSize || 0"></span>B</span></div> + <details class="tx-section"> + <summary class="tx-section-title"><span x-text="`Reveal bundle ${bundle.slot}`"></span><span class="tx-section-meta"><span x-text="short(bundle.member)"></span> · <span x-text="bundle.byte_size || bundle.byteSize || 0"></span>B</span></summary> <template x-for="tx in bundle.reveals" :key="tx.signature"> <div class="tx-card" role="button" tabindex="0" @click="openTransactionModal(tx, { source: 'Reveal bundle', blockHeight: selectedBlock.height, blockFinalizer: bundle.member })" @keydown.enter.prevent="openTransactionModal(tx, { source: 'Reveal bundle', blockHeight: selectedBlock.height, blockFinalizer: bundle.member })" @keydown.space.prevent="openTransactionModal(tx, { source: 'Reveal bundle', blockHeight: selectedBlock.height, blockFinalizer: bundle.member })"> <span class="pill" :class="txPillClass(tx)" x-text="txPillLabel(tx)"></span> @@ -3761,7 +3763,7 @@ const INDEX_HTML: &str = r#"<!doctype html> </div> </template> <div class="muted" x-show="bundle.reveals.length === 0">No reveals in bundle</div> - </div> + </details> </template> <div class="muted" x-show="selectedBlock.transactions.length === 0 && !(selectedBlock.reveal_bundles || selectedBlock.revealBundles || []).length">No transactions</div> </div> @@ -4371,6 +4373,8 @@ mod tests { assert!(super::INDEX_HTML.contains("commitCountLabel(block)")); assert!(super::INDEX_HTML.contains(".pill.blinded")); assert!(super::INDEX_HTML.contains(".pill.reveal, .pill.revealed")); + assert!(super::INDEX_HTML.contains("<details class=\"tx-section\">")); + assert!(super::INDEX_HTML.contains("<summary class=\"tx-section-title\">")); assert!(super::INDEX_HTML.contains("Commitment")); assert!(!super::INDEX_HTML.contains("<h3>Revealed</h3>")); assert!(app_js.contains("tx?.revealed ? \"revealed\"")); diff --git a/src/app.rs b/src/app.rs @@ -474,14 +474,17 @@ impl NodeCore { { continue; } - if !self.ledger.has_blinded_transaction(&commitment) - && self + if !self.ledger.has_blinded_transaction(&commitment) { + match self .ledger - .submit_blinded_transaction(owned.transaction.clone())? - { - self.outbox.push(GossipEnvelope::BlindedTransaction( - owned.transaction.clone(), - )); + .submit_blinded_transaction(owned.transaction.clone()) + { + Ok(true) => self.outbox.push(GossipEnvelope::BlindedTransaction( + owned.transaction.clone(), + )), + Ok(false) => {} + Err(_) => continue, + } } if !self.ledger.has_unrevealed_blinded_transaction(&commitment) { continue; @@ -2852,6 +2855,47 @@ mod tests { } #[test] + fn owned_blinded_transaction_restore_skips_stale_commit_with_spent_input() { + let alice = Wallet::from_seed("owned-blinded-restore-stale-alice"); + let bob = Wallet::from_seed("owned-blinded-restore-stale-bob"); + let finalizers = [bob.clone()]; + let mut allocations = BTreeMap::new(); + allocations.insert(alice.address().to_string(), MICRO_IUNA); + allocations.insert(bob.address().to_string(), MICRO_IUNA); + let ledger = Ledger::new_with_genesis_burns( + allocations, + finalizers + .iter() + .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA)) + .collect(), + 1, + ) + .unwrap(); + let mut wallet_node = NodeCore::from_ledger(alice.clone(), ledger.clone(), 0); + + wallet_node + .blinded_burn_with_fee(MICRO_IUNA / 10, 7, wallet_node.chain_height() + 4) + .unwrap(); + let owned = wallet_node.owned_blinded_transactions(); + let stale_conflict = ledger.build_burn(&alice, MICRO_IUNA / 10, 7).unwrap(); + let mut advanced_ledger = ledger; + advanced_ledger.submit_transaction(stale_conflict).unwrap(); + let leader = advanced_ledger.expected_leader_for_next_block().unwrap(); + assert_eq!(leader, bob.address()); + let anchor_burn = advanced_ledger.build_burn(&bob, 1, 0).unwrap(); + advanced_ledger.submit_transaction(anchor_burn).unwrap(); + let block = advanced_ledger.mine_next_block(&bob, 1).unwrap(); + advanced_ledger.apply_block(block).unwrap(); + let mut restarted = NodeCore::from_ledger(alice, advanced_ledger, 0); + + restarted.restore_owned_blinded_transactions(owned).unwrap(); + + assert!(restarted.owned_blinded_transactions().is_empty()); + assert!(restarted.ledger().pending_blinded_transactions().is_empty()); + assert!(restarted.drain_outbox().is_empty()); + } + + #[test] fn owned_blinded_transaction_restore_publishes_reveal_after_commit() { let alice = Wallet::from_seed("owned-blinded-restore-reveal-alice"); let bob = Wallet::from_seed("owned-blinded-restore-reveal-bob");