iuna

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

commit 253ba778edb3c7f6eca980dfbcc8343e8dca939d
parent 42c882aec6d434bbabf5bb10b0e9f16bea0e05c3
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Tue, 28 Jul 2026 16:22:10 +0200

Validate protocol addresses for canonical fees

Diffstat:
Msrc/app.rs | 16++++++++--------
Msrc/domain.rs | 512++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mtests/iuna.rs | 53+++++++----------------------------------------------
3 files changed, 457 insertions(+), 124 deletions(-)

diff --git a/src/app.rs b/src/app.rs @@ -764,7 +764,7 @@ impl NodeCore { proof_header: Some("00".repeat(80)), signature: "00".repeat(32), }; - let bytes = tx.serialized_size_bytes()?; + let bytes = tx.economic_size_bytes(); let next_fee = fee_per_byte .checked_mul(bytes as Amount) .context("fee per byte times transaction bytes overflows")?; @@ -1428,7 +1428,7 @@ fn converge_fee_by_byte( let mut best = None; for _ in 0..64 { let tx = build(fee)?; - let bytes = tx.serialized_size_bytes()?; + let bytes = tx.economic_size_bytes(); let required_fee = fee_per_byte .checked_mul(bytes as Amount) .context("fee per byte times transaction bytes overflows")?; @@ -1446,7 +1446,7 @@ fn converge_fee_by_byte( } let tx = build(fee)?; - let bytes = tx.serialized_size_bytes()?; + let bytes = tx.economic_size_bytes(); let required_fee = fee_per_byte .checked_mul(bytes as Amount) .context("fee per byte times transaction bytes overflows")?; @@ -1462,7 +1462,7 @@ fn converge_fee_by_byte( } } let tx = build(required_fee)?; - let bytes = tx.serialized_size_bytes()?; + let bytes = tx.economic_size_bytes(); let final_required_fee = fee_per_byte .checked_mul(bytes as Amount) .context("fee per byte times transaction bytes overflows")?; @@ -1544,7 +1544,7 @@ mod tests { }; assert_eq!(anchor, &node.chain().last().unwrap().hash); assert_eq!(output.address, wallet.address()); - let minimum_fee = first_mine.serialized_size_bytes().unwrap() as u64; + let minimum_fee = first_mine.economic_size_bytes() as u64; let (_, expected_estimate) = node .build_mine_with_fee_rate(node.status().mining.automatic_pow_mine_fee) .unwrap(); @@ -1586,7 +1586,7 @@ mod tests { let plan = node.prepare_automatic_mining(1); let mine = plan.pow_mined.expect("PoW should be queued"); - let minimum_fee = mine.serialized_size_bytes().unwrap() as u64 * configured_fee_per_byte; + let minimum_fee = mine.economic_size_bytes() as u64 * configured_fee_per_byte; let (_, expected_estimate) = node .build_mine_with_fee_rate(configured_fee_per_byte) .unwrap(); @@ -1683,11 +1683,11 @@ mod tests { let (transfer, _) = node .transfer_with_fee_rate(bob.address(), MICRO_IUNA, 2, &[]) .unwrap(); - let minimum_transfer_fee = transfer.serialized_size_bytes().unwrap() as u64 * 2; + let minimum_transfer_fee = transfer.economic_size_bytes() as u64 * 2; assert!(transfer.fee() >= minimum_transfer_fee); let (burn, _) = node.burn_with_fee_rate(MICRO_IUNA, 3).unwrap(); - let minimum_burn_fee = burn.serialized_size_bytes().unwrap() as u64 * 3; + let minimum_burn_fee = burn.economic_size_bytes() as u64 * 3; assert!(burn.fee() >= minimum_burn_fee); } } diff --git a/src/domain.rs b/src/domain.rs @@ -32,6 +32,10 @@ const FORK_FINALITY_DEPTH: u64 = 6; const VDF_MODULUS: u128 = 4_611_685_975_477_714_963; const VDF_CHALLENGE_MIN: u64 = 1_073_741_827; const WALLET_SEED_DOMAIN: &str = "iuna-wallet-seed"; +const PUBLIC_KEY_BYTES: usize = 32; +const HASH_BYTES: usize = 32; +const SIGNATURE_BYTES: usize = 64; +const STRATUM_MINE_HEADER_BYTES: usize = 80; #[derive(Clone, Debug, Eq, PartialEq)] pub struct Wallet { @@ -55,7 +59,8 @@ impl Wallet { } fn sign_payload(&self, payload: &str) -> String { - let seed = decode_hex_array::<32>(&self.secret).expect("wallet secret is valid hex"); + let seed = + decode_hex_array::<PUBLIC_KEY_BYTES>(&self.secret).expect("wallet secret is valid hex"); let signing_key = SigningKey::from_bytes(&seed); let signature: Signature = signing_key.sign(payload.as_bytes()); hex_encode(signature.to_bytes()) @@ -231,8 +236,14 @@ impl Transaction { format!("{}:{}", self.signing_payload(), self.signature()) } + pub fn economic_size_bytes(&self) -> usize { + canonical_transaction_size_bytes(self) + } + pub fn serialized_size_bytes(&self) -> Result<usize> { - serialized_transaction_size_bytes(self) + serde_json::to_vec(self) + .map(|bytes| bytes.len()) + .context("failed to serialize transaction for size check") } fn signing_payload(&self) -> String { @@ -321,10 +332,10 @@ impl Transaction { bail!("transaction input signature does not match transaction signature"); } let sender = self.sender(); - let public_key = decode_hex_array::<32>(sender) + let public_key = decode_hex_array::<PUBLIC_KEY_BYTES>(sender) .with_context(|| format!("invalid public key for {sender}"))?; - let signature = - decode_hex_array::<64>(self.signature()).context("invalid signature hex")?; + let signature = decode_hex_array::<SIGNATURE_BYTES>(self.signature()) + .context("invalid signature hex")?; let verifying_key = VerifyingKey::from_bytes(&public_key).context("invalid transaction public key")?; let signature = Signature::from_bytes(&signature); @@ -623,7 +634,7 @@ fn stratum_mine_header_bytes( let mut header = [0_u8; 80]; header[0..4].copy_from_slice(&STRATUM_MINE_VERSION); let anchor_bytes = - decode_hex_array::<32>(anchor).context("mine transaction anchor is not hex")?; + decode_hex_array::<HASH_BYTES>(anchor).context("mine transaction anchor is not hex")?; header[4..36].copy_from_slice(&anchor_bytes); let merkle_root = double_sha256(&stratum_coinbase_bytes( output, @@ -656,6 +667,8 @@ fn stratum_mine_template( difficulty_bits: u32, ) -> Result<StratumMineTemplate> { let recipient = recipient.into(); + validate_address(&recipient, "mine recipient")?; + validate_hash(anchor, "mine transaction anchor")?; if fee > mine_reward { bail!("mine transaction fee exceeds reward"); } @@ -664,7 +677,7 @@ fn stratum_mine_template( amount: mine_reward - fee, }; let anchor_bytes = - decode_hex_array::<32>(anchor).context("mine transaction anchor is not hex")?; + decode_hex_array::<HASH_BYTES>(anchor).context("mine transaction anchor is not hex")?; Ok(StratumMineTemplate { recipient, output_amount: output.amount, @@ -1179,6 +1192,7 @@ impl Ledger { genesis_transactions: Vec<Transaction>, vdf_rounds: u32, ) -> Result<Self> { + validate_genesis_allocations(&genesis_allocations)?; let launch_profile = LaunchProfile::default(); let genesis = build_genesis_block(&genesis_allocations, genesis_transactions); let utxos = utxos_after_genesis(&genesis_allocations, &genesis)?; @@ -1212,6 +1226,7 @@ impl Ledger { bail!("chain snapshot is empty"); } + validate_genesis_allocations(&genesis_allocations)?; let genesis = blocks[0].clone(); validate_genesis_block(&genesis)?; let expected_genesis = @@ -1572,12 +1587,14 @@ impl Ledger { amount: Amount, fee: Amount, ) -> Result<Transaction> { + let to = to.into(); + validate_address(&to, "transfer recipient")?; let required = amount .checked_add(fee) .context("transfer amount plus fee overflows")?; let (inputs, input_total) = self.select_inputs(wallet.address(), required)?; let mut outputs = vec![TxOutput { - address: to.into(), + address: to, amount, }]; let change = input_total @@ -1607,13 +1624,15 @@ impl Ledger { fee: Amount, outpoints: &[OutPoint], ) -> Result<Transaction> { + let to = to.into(); + validate_address(&to, "transfer recipient")?; let required = amount .checked_add(fee) .context("transfer amount plus fee overflows")?; let (inputs, input_total) = self.select_inputs_by_outpoint(wallet.address(), required, outpoints)?; let mut outputs = vec![TxOutput { - address: to.into(), + address: to, amount, }]; let change = input_total @@ -1672,6 +1691,7 @@ impl Ledger { fee: Amount, ) -> Result<Transaction> { let recipient = recipient.into(); + validate_address(&recipient, "mine recipient")?; if fee > self.mine_reward { bail!("mine transaction fee exceeds reward"); } @@ -2123,35 +2143,64 @@ impl Ledger { } fn validate_transaction_terms(&self, transaction: &Transaction) -> Result<()> { - if let Transaction::Mine { - output, - anchor, - difficulty_bits, - fee, - .. - } = transaction - { - if output - .amount - .checked_add(*fee) - .context("mine transaction output plus fee overflows")? - != self.mine_reward - { - bail!("mine transaction reward is invalid"); + match transaction { + Transaction::Transfer { + inputs, + outputs, + signature, + .. + } => { + validate_transaction_inputs(inputs)?; + validate_transaction_outputs(outputs)?; + validate_signature(signature, "transaction signature")?; } - let anchor_block = self - .chain - .iter() - .find(|block| block.hash == *anchor) - .context("mine transaction anchor is not on this chain")?; - let anchor_age = self.tip().height.saturating_sub(anchor_block.height); - if anchor_age > MINE_MAX_ANCHOR_AGE_BLOCKS { - bail!("mine transaction anchor is too old"); + Transaction::Burn { + inputs, + change, + signature, + .. + } => { + validate_transaction_inputs(inputs)?; + validate_transaction_outputs(change)?; + validate_signature(signature, "transaction signature")?; } - let required_difficulty = - self.mine_difficulty_bits_for_anchor_height(anchor_block.height); - if *difficulty_bits != required_difficulty { - bail!("mine transaction difficulty is invalid"); + Transaction::Mine { + output, + anchor, + difficulty_bits, + fee, + proof_header, + signature, + .. + } => { + validate_transaction_outputs(std::slice::from_ref(output))?; + validate_hash(anchor, "mine transaction anchor")?; + validate_hash(signature, "mine transaction proof hash")?; + if let Some(proof_header) = proof_header { + validate_stratum_header(proof_header)?; + } + if output + .amount + .checked_add(*fee) + .context("mine transaction output plus fee overflows")? + != self.mine_reward + { + bail!("mine transaction reward is invalid"); + } + let anchor_block = self + .chain + .iter() + .find(|block| block.hash == *anchor) + .context("mine transaction anchor is not on this chain")?; + let anchor_age = self.tip().height.saturating_sub(anchor_block.height); + if anchor_age > MINE_MAX_ANCHOR_AGE_BLOCKS { + bail!("mine transaction anchor is too old"); + } + let required_difficulty = + self.mine_difficulty_bits_for_anchor_height(anchor_block.height); + if *difficulty_bits != required_difficulty { + bail!("mine transaction difficulty is invalid"); + } } } Ok(()) @@ -2479,8 +2528,8 @@ fn ensure_block_has_burn(transactions: &[Transaction]) -> Result<()> { } fn fee_rate_key(transaction: &Transaction) -> u128 { - let size = serialized_transaction_size_bytes(transaction).unwrap_or(usize::MAX); - if size == 0 || size == usize::MAX { + let size = transaction.economic_size_bytes(); + if size == 0 { return 0; } u128::from(transaction.fee()) * 1_000_000 / size as u128 @@ -2512,10 +2561,195 @@ fn best_selectable_transaction_index( .map(|(index, _)| index) } -fn serialized_transaction_size_bytes(transaction: &Transaction) -> Result<usize> { - serde_json::to_vec(transaction) - .map(|bytes| bytes.len()) - .context("failed to serialize transaction for size check") +fn validate_genesis_allocations(genesis_allocations: &BTreeMap<String, Amount>) -> Result<()> { + for address in genesis_allocations.keys() { + validate_address(address, "genesis allocation")?; + } + Ok(()) +} + +fn validate_transaction_inputs(inputs: &[TxInput]) -> Result<()> { + for input in inputs { + validate_protocol_id(&input.outpoint.txid, "input outpoint txid")?; + validate_address(&input.owner, "input owner")?; + validate_signature(&input.signature, "input signature")?; + } + Ok(()) +} + +fn validate_transaction_outputs(outputs: &[TxOutput]) -> Result<()> { + for output in outputs { + validate_address(&output.address, "output recipient")?; + } + Ok(()) +} + +fn validate_genesis_burn_transaction(transaction: &Transaction) -> Result<()> { + let Transaction::Burn { + inputs, + change, + fee, + signature, + .. + } = transaction + else { + bail!("genesis only supports burn transactions"); + }; + if *fee != 0 { + bail!("genesis burn fee must be zero"); + } + validate_hash(signature, "genesis burn signature")?; + validate_transaction_outputs(change)?; + for input in inputs { + validate_hash(&input.outpoint.txid, "genesis burn input outpoint txid")?; + validate_address(&input.owner, "genesis burn input owner")?; + if input.signature != "genesis" { + bail!("genesis burn input signature is invalid"); + } + } + Ok(()) +} + +fn validate_address(address: &str, label: &str) -> Result<()> { + decode_hex_array::<PUBLIC_KEY_BYTES>(address) + .with_context(|| format!("invalid {label} address"))?; + Ok(()) +} + +fn validate_hash(hash: &str, label: &str) -> Result<()> { + decode_hex_array::<HASH_BYTES>(hash).with_context(|| format!("invalid {label}"))?; + Ok(()) +} + +fn validate_signature(signature: &str, label: &str) -> Result<()> { + decode_hex_array::<SIGNATURE_BYTES>(signature).with_context(|| format!("invalid {label}"))?; + Ok(()) +} + +fn validate_stratum_header(header: &str) -> Result<()> { + decode_hex_array::<STRATUM_MINE_HEADER_BYTES>(header) + .context("invalid mine transaction proof header")?; + Ok(()) +} + +fn validate_protocol_id(value: &str, label: &str) -> Result<()> { + let bytes = decode_hex(value).with_context(|| format!("invalid {label}"))?; + match bytes.len() { + HASH_BYTES | SIGNATURE_BYTES => Ok(()), + length => bail!("invalid {label}: expected 32 or 64 bytes, got {length}"), + } +} + +fn canonical_transaction_size_bytes(transaction: &Transaction) -> usize { + match transaction { + Transaction::Transfer { + inputs, + outputs, + fee, + signature, + } => { + 1 + compact_len(inputs.len() as u128) + + compact_inputs_size_bytes(inputs) + + compact_len(outputs.len() as u128) + + compact_outputs_size_bytes(outputs) + + compact_len(u128::from(*fee)) + + signature_size_bytes(signature) + } + Transaction::Burn { + inputs, + change, + amount, + fee, + signature, + } => { + 1 + compact_len(inputs.len() as u128) + + compact_inputs_size_bytes(inputs) + + compact_len(change.len() as u128) + + compact_outputs_size_bytes(change) + + compact_len(u128::from(*amount)) + + compact_len(u128::from(*fee)) + + signature_size_bytes(signature) + } + Transaction::Mine { + output, + anchor, + salt, + nonce, + difficulty_bits, + fee, + proof_header, + signature, + } => { + 1 + compact_output_size_bytes(output) + + hash_size_bytes(anchor) + + compact_len(u128::from(*salt)) + + compact_len(u128::from(*nonce)) + + compact_len(u128::from(*difficulty_bits)) + + compact_len(u128::from(*fee)) + + 1 + + proof_header + .as_ref() + .map(|header| stratum_header_size_bytes(header)) + .unwrap_or(0) + + hash_size_bytes(signature) + } + } +} + +fn compact_inputs_size_bytes(inputs: &[TxInput]) -> usize { + inputs + .iter() + .map(|input| { + protocol_id_size_bytes(&input.outpoint.txid) + + compact_len(u128::from(input.outpoint.index)) + + address_size_bytes(&input.owner) + }) + .sum() +} + +fn compact_outputs_size_bytes(outputs: &[TxOutput]) -> usize { + outputs.iter().map(compact_output_size_bytes).sum() +} + +fn compact_output_size_bytes(output: &TxOutput) -> usize { + address_size_bytes(&output.address) + compact_len(u128::from(output.amount)) +} + +fn address_size_bytes(address: &str) -> usize { + debug_assert!(validate_address(address, "debug address").is_ok()); + PUBLIC_KEY_BYTES +} + +fn hash_size_bytes(hash: &str) -> usize { + debug_assert!(validate_hash(hash, "debug hash").is_ok()); + HASH_BYTES +} + +fn signature_size_bytes(signature: &str) -> usize { + debug_assert!(validate_signature(signature, "debug signature").is_ok()); + SIGNATURE_BYTES +} + +fn stratum_header_size_bytes(header: &str) -> usize { + debug_assert!(validate_stratum_header(header).is_ok()); + STRATUM_MINE_HEADER_BYTES +} + +fn protocol_id_size_bytes(value: &str) -> usize { + match decode_hex(value).map(|bytes| bytes.len()) { + Ok(HASH_BYTES) => HASH_BYTES, + Ok(SIGNATURE_BYTES) => SIGNATURE_BYTES, + _ => SIGNATURE_BYTES, + } +} + +fn compact_len(mut value: u128) -> usize { + let mut bytes = 1; + while value >= 0x80 { + value >>= 7; + bytes += 1; + } + bytes } fn estimated_block_size_bytes(transactions: &[Transaction]) -> Result<usize> { @@ -2573,10 +2807,10 @@ fn verify_leader_proof(block: &Block, tickets: &[BurnTicket]) -> Result<()> { } fn verify_leader_signature(proof: &LeaderProof, payload: &LeaderProofPayload) -> Result<()> { - let public_key = decode_hex_array::<32>(&proof.public_key) + let public_key = decode_hex_array::<PUBLIC_KEY_BYTES>(&proof.public_key) .with_context(|| format!("invalid leader public key {}", proof.public_key))?; - let signature = - decode_hex_array::<64>(&proof.signature).context("invalid leader signature hex")?; + let signature = decode_hex_array::<SIGNATURE_BYTES>(&proof.signature) + .context("invalid leader signature hex")?; let verifying_key = VerifyingKey::from_bytes(&public_key).context("invalid leader public key")?; let signature = Signature::from_bytes(&signature); @@ -2764,7 +2998,10 @@ fn utxos_after_genesis( let mut utxos = genesis_allocation_utxos(genesis_allocations); for transaction in &genesis.transactions { match transaction { - Transaction::Burn { .. } => apply_transaction(transaction, &mut utxos)?, + Transaction::Burn { .. } => { + validate_genesis_burn_transaction(transaction)?; + apply_transaction(transaction, &mut utxos)?; + } Transaction::Transfer { .. } | Transaction::Mine { .. } => { bail!("genesis only supports burn transactions") } @@ -3056,6 +3293,20 @@ fn hex_encode(bytes: impl AsRef<[u8]>) -> String { mod tests { use super::*; + fn test_utxo_outpoint(index: usize) -> OutPoint { + OutPoint { + txid: format!("{index:064x}"), + index: 0, + } + } + + fn named_test_outpoint(name: &str) -> OutPoint { + OutPoint { + txid: hex_hash(format!("test-utxo:{name}")), + index: 0, + } + } + fn ledger_with_wallet_utxos(wallet: &Wallet, amounts: &[Amount]) -> Ledger { let mut ledger = Ledger::new(BTreeMap::new(), 1); ledger.utxos = amounts @@ -3063,10 +3314,7 @@ mod tests { .enumerate() .map(|(index, amount)| { ( - OutPoint { - txid: format!("test-utxo-{index}"), - index: 0, - }, + test_utxo_outpoint(index), TxOutput { address: wallet.address().to_string(), amount: *amount, @@ -3129,16 +3377,46 @@ mod tests { panic!("expected to find mine proof"); } + fn transfer_with_extra_zero_outputs( + ledger: &Ledger, + wallet: &Wallet, + to: &str, + amount: Amount, + fee: Amount, + extra_outputs: usize, + ) -> Transaction { + let required = amount.checked_add(fee).unwrap(); + let (inputs, input_total) = ledger.select_inputs(wallet.address(), required).unwrap(); + let mut outputs = vec![TxOutput { + address: to.to_string(), + amount, + }]; + outputs.extend((0..extra_outputs).map(|_| TxOutput { + address: to.to_string(), + amount: 0, + })); + let change = input_total - required; + if change > 0 { + outputs.push(TxOutput { + address: wallet.address().to_string(), + amount: change, + }); + } + UnsignedUtxoTransaction::Transfer { + inputs, + outputs, + fee, + } + .sign(wallet) + } + #[test] fn wallet_utxos_only_include_outputs_owned_by_address() { let alice = Wallet::from_seed("wallet-utxos-alice"); let bob = Wallet::from_seed("wallet-utxos-bob"); let mut ledger = ledger_with_wallet_utxos(&alice, &[2, 3]); ledger.utxos.insert( - OutPoint { - txid: "bob-utxo".to_string(), - index: 0, - }, + named_test_outpoint("bob"), TxOutput { address: bob.address().to_string(), amount: 5, @@ -3232,14 +3510,117 @@ mod tests { } #[test] + fn transaction_economic_size_uses_compact_canonical_fields() { + let alice = Wallet::from_seed("economic-size-alice"); + let bob = Wallet::from_seed("economic-size-bob"); + let ledger = ledger_with_wallet_utxos(&alice, &[1, 1, 2]); + let selected = vec![ + test_utxo_outpoint(0), + test_utxo_outpoint(1), + test_utxo_outpoint(2), + ]; + + let tx = ledger + .build_transfer_with_inputs(&alice, bob.address(), 3, 0, &selected) + .unwrap(); + + assert!(tx.economic_size_bytes() < tx.serialized_size_bytes().unwrap()); + assert_eq!( + tx.economic_size_bytes(), + 1 + 1 + (3 * (32 + 1 + 32)) + 1 + (2 * (32 + 1)) + 1 + 64 + ); + } + + #[test] + fn transfer_rejects_invalid_recipient_address() { + let alice = Wallet::from_seed("invalid-transfer-recipient-alice"); + let ledger = ledger_with_wallet_utxos(&alice, &[10]); + + let error = ledger.build_transfer(&alice, "aa", 1, 0).unwrap_err(); + + assert!(format!("{error:#}").contains("invalid transfer recipient address")); + } + + #[test] + fn mine_rejects_invalid_recipient_address_before_pow() { + let ledger = Ledger::new(BTreeMap::new(), 1); + + let error = ledger.build_mine("aa").unwrap_err(); + + assert!(format!("{error:#}").contains("invalid mine recipient address")); + } + + #[test] + fn mempool_rejects_invalid_input_outpoint_id() { + let alice = Wallet::from_seed("invalid-outpoint-alice"); + let bob = Wallet::from_seed("invalid-outpoint-bob"); + let mut ledger = ledger_with_wallet_utxos(&alice, &[10]); + let unsigned = UnsignedUtxoTransaction::Transfer { + inputs: vec![UnsignedTxInput { + outpoint: OutPoint { + txid: "aa".to_string(), + index: 0, + }, + owner: alice.address().to_string(), + }], + outputs: vec![TxOutput { + address: bob.address().to_string(), + amount: 1, + }], + fee: 0, + }; + let transaction = unsigned.sign(&alice); + + let error = ledger.submit_transaction(transaction).unwrap_err(); + + assert!(format!("{error:#}").contains("invalid input outpoint txid")); + assert!(ledger.pending().is_empty()); + } + + #[test] + fn miner_skips_oversized_pending_transaction_and_keeps_fitting_fee_transaction() { + let alice = Wallet::from_seed("oversized-select-alice"); + let bob = Wallet::from_seed("oversized-select-bob"); + let carol = Wallet::from_seed("oversized-select-carol"); + let mut allocations = BTreeMap::new(); + allocations.insert(alice.address().to_string(), 1); + allocations.insert(bob.address().to_string(), 300_000); + allocations.insert(carol.address().to_string(), 300_000); + let mut ledger = Ledger::new_with_genesis_burns( + allocations, + vec![GenesisBurn::new(alice.address(), 1)], + 10, + ) + .unwrap(); + let burn = ledger.build_burn(&alice, 1, 0).unwrap(); + ledger.submit_transaction(burn).unwrap(); + let oversized = + transfer_with_extra_zero_outputs(&ledger, &bob, alice.address(), 1, 100_000, 4_000); + let fitting = ledger + .build_transfer(&carol, alice.address(), 1, 5) + .unwrap(); + assert!(oversized.serialized_size_bytes().unwrap() > MAX_BLOCK_BYTES); + ledger.submit_transaction(oversized.clone()).unwrap(); + ledger.submit_transaction(fitting.clone()).unwrap(); + + let block = ledger.mine_next_block(&alice, 1).unwrap(); + let signatures = block + .transactions + .iter() + .map(|tx| tx.signature().to_string()) + .collect::<Vec<_>>(); + + assert!(!signatures.contains(&oversized.signature().to_string())); + assert!(signatures.contains(&fitting.signature().to_string())); + assert!(block.serialized_size_bytes().unwrap() <= MAX_BLOCK_BYTES); + } + + #[test] fn transfer_can_spend_selected_utxos_when_they_cover_amount_and_fee() { let alice = Wallet::from_seed("selected-utxos-alice"); let bob = Wallet::from_seed("selected-utxos-bob"); let mut ledger = ledger_with_wallet_utxos(&alice, &[2, 3, 5]); - let selected = vec![OutPoint { - txid: "test-utxo-2".to_string(), - index: 0, - }]; + let selected = vec![test_utxo_outpoint(2)]; let tx = ledger .build_transfer_with_inputs(&alice, bob.address(), 2, 1, &selected) @@ -3278,10 +3659,7 @@ mod tests { let alice = Wallet::from_seed("selected-utxos-insufficient-alice"); let bob = Wallet::from_seed("selected-utxos-insufficient-bob"); let ledger = ledger_with_wallet_utxos(&alice, &[2, 3, 5]); - let selected = vec![OutPoint { - txid: "test-utxo-0".to_string(), - index: 0, - }]; + let selected = vec![test_utxo_outpoint(0)]; let error = ledger .build_transfer_with_inputs(&alice, bob.address(), 2, 1, &selected) @@ -3297,19 +3675,13 @@ mod tests { let carol = Wallet::from_seed("selected-utxos-owner-carol"); let mut ledger = ledger_with_wallet_utxos(&alice, &[5]); ledger.utxos.insert( - OutPoint { - txid: "carol-utxo".to_string(), - index: 0, - }, + named_test_outpoint("carol"), TxOutput { address: carol.address().to_string(), amount: 5, }, ); - let selected = vec![OutPoint { - txid: "carol-utxo".to_string(), - index: 0, - }]; + let selected = vec![named_test_outpoint("carol")]; let error = ledger .build_transfer_with_inputs(&alice, bob.address(), 2, 1, &selected) diff --git a/tests/iuna.rs b/tests/iuna.rs @@ -459,7 +459,10 @@ fn oversized_blocks_are_rejected() { .unwrap(); submit_burn(&mut ledger, &alice, 1); let mut block = ledger.mine_next_block(&alice, 1).unwrap(); - let oversized_transfer = transfer_fee_tx(&ledger, &bob, "x".repeat(MAX_BLOCK_BYTES), 1, 3); + let mut oversized_transfer = transfer_fee_tx(&ledger, &bob, alice.address(), 1, 3); + if let iuna::domain::Transaction::Transfer { outputs, .. } = &mut oversized_transfer { + outputs[0].address = "x".repeat(MAX_BLOCK_BYTES); + } block.transactions.push(oversized_transfer); block.reward = 3; block.hash = block.compute_hash(); @@ -470,39 +473,6 @@ fn oversized_blocks_are_rejected() { } #[test] -fn miner_skips_oversized_pending_transaction_and_keeps_fitting_fee_transaction() { - let wallets = wallets(&[ - "oversized-select-alice", - "oversized-select-bob", - "oversized-select-carol", - ]); - let alice = &wallets[0]; - let bob = &wallets[1]; - let carol = &wallets[2]; - let mut allocations = allocations(&wallets, 200_000); - allocations.insert(alice.address().to_string(), 1); - let mut ledger = - Ledger::new_with_genesis_burns(allocations, vec![GenesisBurn::new(alice.address(), 1)], 10) - .unwrap(); - submit_burn(&mut ledger, alice, 1); - let oversized = transfer_fee_tx(&ledger, bob, "x".repeat(MAX_BLOCK_BYTES), 1, 100_000); - let fitting = transfer_fee_tx(&ledger, carol, alice.address(), 1, 5); - ledger.submit_transaction(oversized.clone()).unwrap(); - ledger.submit_transaction(fitting.clone()).unwrap(); - - let block = ledger.mine_next_block(alice, 1).unwrap(); - let signatures = block - .transactions - .iter() - .map(|tx| tx.signature().to_string()) - .collect::<Vec<_>>(); - - assert!(!signatures.contains(&oversized.signature().to_string())); - assert!(signatures.contains(&fitting.signature().to_string())); - assert!(block.serialized_size_bytes().unwrap() <= MAX_BLOCK_BYTES); -} - -#[test] fn transfer_that_would_overflow_recipient_balance_is_rejected() { let alice = Wallet::from_seed("alice"); let bob = Wallet::from_seed("bob"); @@ -607,10 +577,7 @@ fn automatic_mining_burns_configured_amount_once_per_height() { assert!(second.burned.is_some()); let burned = second.burned.as_ref().unwrap(); assert_eq!(burned.amount(), iuna(25)); - assert_eq!( - burned.fee(), - burned.serialized_size_bytes().unwrap() as u64 * DEFAULT_FEE_PER_BYTE - ); + assert!(burned.fee() >= burned.economic_size_bytes() as u64 * DEFAULT_FEE_PER_BYTE); } #[test] @@ -678,10 +645,7 @@ fn automatic_mining_uses_configured_burn_fee() { let burned = node.set_automatic_burn(50, 3).unwrap().unwrap(); assert_eq!(burned.amount(), 50); - assert_eq!( - burned.fee(), - burned.serialized_size_bytes().unwrap() as u64 * 3 - ); + assert_eq!(burned.fee(), burned.economic_size_bytes() as u64 * 3); } #[test] @@ -702,10 +666,7 @@ fn automatic_mining_caps_burn_to_spendable_balance_after_fee() { let burned = outcome.burned.as_ref().unwrap(); let unspent = BLOCK_REWARD - burned.amount() - burned.fee(); assert!(unspent <= DEFAULT_FEE_PER_BYTE); - assert_eq!( - burned.fee(), - burned.serialized_size_bytes().unwrap() as u64 * DEFAULT_FEE_PER_BYTE - ); + assert!(burned.fee() >= burned.economic_size_bytes() as u64 * DEFAULT_FEE_PER_BYTE); assert!(outcome.block.is_some()); assert_eq!( node.ledger().balance_of(alice.address()),