iuna

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

commit 3ac9f5b5b76641402723899066abe7d4cae3556e
parent ca36ec19fb6ff5b185034a757cf7ff79bdbbba7e
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Tue,  4 Aug 2026 11:30:29 +0200

Release v0.2.15

Diffstat:
MCargo.lock | 2+-
MCargo.toml | 2+-
MREADME.md | 2+-
Mdocs/protocol.md | 11++++++++---
Msrc/domain.rs | 122++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mwww/index.html | 2+-
6 files changed, 124 insertions(+), 17 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -537,7 +537,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "iuna" -version = "0.2.14" +version = "0.2.15" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iuna" -version = "0.2.14" +version = "0.2.15" edition = "2024" license = "Apache-2.0" diff --git a/README.md b/README.md @@ -21,7 +21,7 @@ Most chains lean heavily on one scarce resource: - Proof-of-work chains rely on hashpower. - Proof-of-stake chains rely on existing stake. -iuna tries a different split. Finalization is lightweight and based on a burn lottery plus VDF timing, while new supply stays open to proof-of-work. The intended benefit is better decentralization pressure than pure PoW or pure PoS: finalizing blocks should not require owning specialized mining scale, and issuing new coins should not require already being a large holder. +iuna tries a different split. Finalization is lightweight and based on a burn lottery plus VDF timing, while new supply stays open to proof-of-work. Burns do not remove wealth advantage, but they make finalization power temporary and repeatedly paid for instead of a permanent stake position. The intended benefit is better decentralization pressure than pure PoW or pure PoS: finalizing blocks should not require owning specialized mining scale, and burn-based timing power should expire instead of accumulating into lasting control. This is still an experiment. The design needs real-world testing before those goals can be treated as proven. diff --git a/docs/protocol.md b/docs/protocol.md @@ -8,6 +8,8 @@ iuna is an experimental devnet protocol that combines three mechanisms: The goal is to avoid relying on only one scarce resource. Proof-of-work chains tend to centralize around hardware and cheap energy. Proof-of-stake chains tend to centralize around existing wealth and staking pools. iuna tries a split design: burns choose who finalizes blocks, VDFs pace block production, and PoW keeps new issuance open to anyone who can find valid work. +Burns do not remove wealth advantage. More capital can still buy more lottery weight. The difference from stake is that burn power is paid again and again: it expires, does not unbond, and does not accumulate into a permanent stake position. The design converts wealth-bias from a growing asset into a recurring cost. + This is still experimental. The rules below describe the current devnet protocol, not a proven mainnet design. ## Coins and Transactions @@ -46,7 +48,9 @@ The selected finalizer must prove ownership of the selected ticket, respect its Every normal block must include at least one plaintext burn. A blinded transaction envelope does not satisfy that rule, because the finalizer and validators cannot know whether the encrypted payload is a burn until reveal. Block-producing nodes create this plaintext burn locally from the finalizer wallet during block construction; it is not part of the gossiped mempool. -Plaintext transaction fees go to the block finalizer immediately. Blinded transaction fees are paid to the finalizer that committed the envelope when the payload is revealed and executed. +This mandatory burn is a liveness rule for the ticket pool, not a fairness rule for ticket distribution. It guarantees that normal block production keeps creating future tickets. Fairness against self-serving finalizers comes from blinded third-party burns. + +Plaintext transaction fees go to the block finalizer immediately. Blinded transaction fees are paid when the payload is revealed and executed: half goes to the finalizer that originally committed the envelope, and the rest goes to the finalizer that includes the reveal. For odd micro-unit fees, the extra micro-unit goes to the reveal executor. ## VDF Timing @@ -123,11 +127,11 @@ Normal mempool traffic uses blinded transaction content. A wallet encrypts a nor - expiry height; - nonce, ciphertext, and plaintext payload hash. -The finalizer can rank the envelope by fee per encrypted byte, but cannot see whether the encrypted payload is a transfer or a burn before committing it to a block. +The finalizer can rank the envelope by fee per visible envelope byte, but cannot see whether the encrypted payload is a transfer or a burn before committing it to a block. Reveal is a later step. A `BlindedReveal` carries only the commitment and decryption key. When a valid reveal is included, nodes decrypt the earlier payload, check the commitment and payload hash, decode the normal transaction, validate it against the current UTXO set, and execute it. If the decrypted transaction is a burn, it creates burn tickets at the reveal height, not the earlier envelope-commit height. -Fees are paid without inflating the reveal block reward. The decrypted transaction must pay the same fee declared by the blinded envelope. When it executes, that fee is credited to the finalizer that originally included the blinded envelope, using a deterministic fee output tied to the commitment. +Fees are paid without inflating the reveal block reward. The decrypted transaction must pay the same fee declared by the blinded envelope. When it executes, the fee is split using deterministic fee outputs tied to the commitment: `floor(fee / 2)` to the envelope committer and `ceil(fee / 2)` to the reveal executor. If both roles are held by the same finalizer, that finalizer receives the full fee through the two deterministic outputs. Expiry is exclusive: a blinded envelope with expiry height `H` can be included only in blocks below height `H`, and revealed only while the current chain height is below `H`. The expiry height must be within `20` blocks of the node's current chain height when the envelope is accepted or selected. Expired envelopes and reveals are dropped from local selection. @@ -167,6 +171,7 @@ iuna is trying to make these things true at the same time: - Finalization should not require specialized mining hardware. - New issuance should not require already owning a large stake. - Burns should have real opportunity cost. +- Burn timing power should expire rather than accumulate into permanent control. - Block timing should be hard to rush. - Finalizers should have a consensus-level reason to include burn traffic they cannot inspect before committing. diff --git a/src/domain.rs b/src/domain.rs @@ -2450,7 +2450,7 @@ impl Ledger { .context("blinded reveal does not reference an active blinded transaction")?; let tx = self.decrypt_active_blinded(active, reveal)?; apply_transaction(&tx, &mut utxos)?; - credit_blinded_fee_output(&mut utxos, active, &tx)?; + credit_blinded_fee_outputs(&mut utxos, active, &block.miner, &tx)?; revealed_transactions.push(tx); } if block.reward != fee_reward(&block.transactions)? { @@ -4051,21 +4051,45 @@ fn blinded_transaction_commitment(transaction: &BlindedTransaction) -> Result<St Ok(hex_hash(without_commitment.canonical())) } -fn credit_blinded_fee_output( +fn credit_blinded_fee_outputs( utxos: &mut BTreeMap<OutPoint, TxOutput>, active: &ActiveBlindedTransaction, + reveal_executor: &str, transaction: &Transaction, ) -> Result<()> { let fee = transaction.fee(); if fee == 0 { return Ok(()); } - let output = TxOutput { - address: active.included_by.clone(), - amount: fee, - }; - ensure_outputs_do_not_overflow(utxos, std::slice::from_ref(&output))?; - utxos.insert(blinded_fee_outpoint(&active.transaction.commitment), output); + let committer_fee = fee / 2; + let executor_fee = fee - committer_fee; + let mut outputs = Vec::new(); + if committer_fee > 0 { + outputs.push(( + blinded_committer_fee_outpoint(&active.transaction.commitment), + TxOutput { + address: active.included_by.clone(), + amount: committer_fee, + }, + )); + } + if executor_fee > 0 { + outputs.push(( + blinded_executor_fee_outpoint(&active.transaction.commitment), + TxOutput { + address: reveal_executor.to_string(), + amount: executor_fee, + }, + )); + } + let tx_outputs = outputs + .iter() + .map(|(_, output)| output.clone()) + .collect::<Vec<_>>(); + ensure_outputs_do_not_overflow(utxos, &tx_outputs)?; + for (outpoint, output) in outputs { + utxos.insert(outpoint, output); + } Ok(()) } @@ -4251,13 +4275,20 @@ fn reward_outpoint(block_hash: &str) -> OutPoint { } } -fn blinded_fee_outpoint(commitment: &str) -> OutPoint { +fn blinded_committer_fee_outpoint(commitment: &str) -> OutPoint { OutPoint { txid: commitment.to_string(), index: u32::MAX - 1, } } +fn blinded_executor_fee_outpoint(commitment: &str) -> OutPoint { + OutPoint { + txid: commitment.to_string(), + index: u32::MAX - 2, + } +} + fn validate_genesis_block(block: &Block) -> Result<()> { if block.height != 0 { bail!("genesis block height must be 0"); @@ -4922,6 +4953,44 @@ mod tests { } #[test] + fn blinded_fee_split_rounds_remainder_to_reveal_executor() { + let committer = Wallet::from_seed("blinded-split-committer"); + let executor = Wallet::from_seed("blinded-split-executor"); + let commitment = "01".repeat(32); + let active = ActiveBlindedTransaction { + transaction: BlindedTransaction { + commitment: commitment.clone(), + fee: 1, + encrypted_size: 1, + expires_at_height: 2, + nonce: "02".repeat(BLINDED_NONCE_BYTES), + ciphertext: "03".to_string(), + payload_hash: "04".repeat(32), + }, + included_height: 1, + included_by: committer.address().to_string(), + }; + let transaction = Transaction::Transfer { + inputs: Vec::new(), + outputs: Vec::new(), + fee: 1, + signature: String::new(), + }; + let mut utxos = BTreeMap::new(); + + credit_blinded_fee_outputs(&mut utxos, &active, executor.address(), &transaction).unwrap(); + + assert!(!utxos.contains_key(&blinded_committer_fee_outpoint(&commitment))); + assert_eq!( + utxos.get(&blinded_executor_fee_outpoint(&commitment)), + Some(&TxOutput { + address: executor.address().to_string(), + amount: 1, + }) + ); + } + + #[test] fn transfer_rejects_invalid_recipient_address() { let alice = Wallet::from_seed("invalid-transfer-recipient-alice"); let ledger = ledger_with_wallet_utxos(&alice, &[10]); @@ -5678,6 +5747,7 @@ mod tests { ledger.submit_blinded_reveal(blinded.reveal).unwrap(); queue_next_leader_burn(&mut ledger, &finalizers); let reveal_block = mine_preverified_as_next_leader(&mut ledger, &finalizers, 2); + let reveal_executor = reveal_block.miner.clone(); assert_eq!(reveal_block.blinded_reveals.len(), 1); assert_eq!( @@ -5699,9 +5769,41 @@ mod tests { .fold(0_u64, |total, transaction| { total + transaction.amount() + transaction.fee() }); + let committer_fee = fee / 2; + let executor_fee = fee - committer_fee; + assert_eq!( + ledger + .utxos + .get(&blinded_committer_fee_outpoint( + &commit_block.blinded_transactions[0].commitment + )) + .unwrap(), + &TxOutput { + address: inclusion_finalizer.clone(), + amount: committer_fee, + } + ); + assert_eq!( + ledger + .utxos + .get(&blinded_executor_fee_outpoint( + &commit_block.blinded_transactions[0].commitment + )) + .unwrap(), + &TxOutput { + address: reveal_executor.clone(), + amount: executor_fee, + } + ); + let inclusion_finalizer_fee = if inclusion_finalizer == reveal_executor { + fee + } else { + committer_fee + }; assert_eq!( ledger.balance_of(&inclusion_finalizer), - before_inclusion_finalizer + fee - reveal_plaintext_burn_spent_by_inclusion_finalizer + before_inclusion_finalizer + inclusion_finalizer_fee + - reveal_plaintext_burn_spent_by_inclusion_finalizer ); } diff --git a/www/index.html b/www/index.html @@ -346,7 +346,7 @@ <div class="wrap"> <div class="section-head"> <h2>What Is iuna?</h2> - <p>iuna combines proof-of-burn finalization, a VDF clock, and proof-of-work issuance. It aims for broader decentralization than systems based only on hashpower or only on existing stake.</p> + <p>iuna combines proof-of-burn finalization, a VDF clock, and proof-of-work issuance. Burns turn capital into temporary timing power that must be paid for again, rather than a permanent stake position.</p> </div> <div class="grid"> <div class="card">