commit 761bdb43dcca456dffa035736b4819755f0361c8
parent dadc1ff339eb5f21042208d4055473d833bc51bd
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Thu, 13 Aug 2026 22:35:15 +0200
Reject transactions too large for blocks
Diffstat:
4 files changed, 62 insertions(+), 18 deletions(-)
diff --git a/src/domain/ledger_mempool.rs b/src/domain/ledger_mempool.rs
@@ -1,7 +1,9 @@
use anyhow::{Result, bail};
use super::ledger_ops::{
- apply_transaction, spend_blinded_inputs, spend_inputs, transaction_has_missing_inputs,
+ apply_transaction, ensure_blinded_transaction_fits_empty_block,
+ ensure_transaction_fits_empty_block, spend_blinded_inputs, spend_inputs,
+ transaction_has_missing_inputs,
};
use super::transaction::{
BlindedReveal, BlindedTransaction, Transaction, blinded_transaction_inputs_spent_by,
@@ -27,6 +29,10 @@ impl Ledger {
return Ok(false);
}
self.validate_blinded_transaction(&transaction)?;
+ ensure_blinded_transaction_fits_empty_block(
+ &transaction,
+ self.launch_profile.max_block_bytes,
+ )?;
if blinded_transaction_inputs_spent_by(&transaction, &self.pending_blinded)
|| transaction_inputs_spent_by_inputs(&transaction.inputs, &self.pending)
|| transaction_inputs_spent_by_inputs(&transaction.inputs, &self.orphans)
@@ -64,6 +70,7 @@ impl Ledger {
transaction.verify_signature()?;
self.validate_transaction_terms(&transaction)?;
+ ensure_transaction_fits_empty_block(&transaction, self.launch_profile.max_block_bytes)?;
self.validate_mine_anchor_available(&transaction)?;
if transaction_inputs_spent_by(&transaction, &self.pending) {
diff --git a/src/domain/ledger_ops.rs b/src/domain/ledger_ops.rs
@@ -98,6 +98,34 @@ pub(super) fn estimated_block_selection_size_bytes(
block.serialized_size_bytes()
}
+pub(super) fn ensure_transaction_fits_empty_block(
+ transaction: &Transaction,
+ max_block_bytes: usize,
+) -> Result<()> {
+ let selection = BlockSelection {
+ transactions: vec![transaction.clone()],
+ blinded_transactions: Vec::new(),
+ };
+ if estimated_block_selection_size_bytes(&selection, false)? > max_block_bytes {
+ bail!("transaction exceeds max block size");
+ }
+ Ok(())
+}
+
+pub(super) fn ensure_blinded_transaction_fits_empty_block(
+ transaction: &BlindedTransaction,
+ max_block_bytes: usize,
+) -> Result<()> {
+ let selection = BlockSelection {
+ transactions: Vec::new(),
+ blinded_transactions: vec![transaction.clone()],
+ };
+ if estimated_block_selection_size_bytes(&selection, false)? > max_block_bytes {
+ bail!("blinded transaction exceeds max block size");
+ }
+ Ok(())
+}
+
pub(super) fn verify_leader_proof(block: &Block, tickets: &[BurnTicket]) -> Result<()> {
let Some(proof) = &block.leader_proof else {
bail!("block is missing leader proof");
diff --git a/src/domain/ledger_pending.rs b/src/domain/ledger_pending.rs
@@ -10,7 +10,8 @@ use super::blinded::{
use super::ledger_ops::{
apply_spendable_pending_transaction, apply_transaction, best_selectable_blinded_index,
best_selectable_burn_from_index, best_selectable_transaction_index,
- ensure_outputs_do_not_overflow, ensure_single_input_owner,
+ ensure_blinded_transaction_fits_empty_block, ensure_outputs_do_not_overflow,
+ ensure_single_input_owner, ensure_transaction_fits_empty_block,
estimated_block_selection_size_bytes, spend_blinded_inputs, spend_spendable_blinded_inputs,
transaction_has_missing_inputs, validate_transaction_inputs, validate_transaction_outputs,
};
@@ -291,6 +292,7 @@ impl Ledger {
pub(super) fn validate_new_transaction(&self, transaction: &Transaction) -> Result<()> {
self.validate_transaction_terms(transaction)?;
+ ensure_transaction_fits_empty_block(transaction, self.launch_profile.max_block_bytes)?;
self.validate_mine_anchor_available(transaction)?;
let mut utxos = self.utxos_after_spendable_pending()?;
apply_transaction(transaction, &mut utxos)
@@ -445,6 +447,10 @@ impl Ledger {
if transaction.commitment != expected {
bail!("blinded transaction commitment is invalid");
}
+ ensure_blinded_transaction_fits_empty_block(
+ transaction,
+ self.launch_profile.max_block_bytes,
+ )?;
Ok(())
}
diff --git a/src/domain/tests.rs b/src/domain/tests.rs
@@ -1098,14 +1098,12 @@ fn block_before_finalizer_rank_time_slot_is_rejected() {
}
#[test]
-fn miner_skips_oversized_pending_transaction_and_keeps_fitting_fee_transaction() {
+fn mempool_rejects_transaction_that_cannot_fit_in_a_block() {
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();
@@ -1113,23 +1111,28 @@ fn miner_skips_oversized_pending_transaction_and_keeps_fitting_fee_transaction()
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())
+ let error = ledger.submit_transaction(oversized).unwrap_err();
+
+ assert!(format!("{error:#}").contains("transaction exceeds max block size"));
+}
+
+#[test]
+fn transfer_builder_rejects_selected_utxos_that_make_transaction_too_large() {
+ let alice = Wallet::from_seed("oversized-selected-utxos-alice");
+ let bob = Wallet::from_seed("oversized-selected-utxos-bob");
+ let amounts = vec![1; 2_000];
+ let ledger = ledger_with_wallet_utxos(&alice, &amounts);
+ let selected = (0..amounts.len())
+ .map(test_utxo_outpoint)
.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);
+ let error = ledger
+ .build_transfer_with_inputs(&alice, bob.address(), 1, 0, &selected)
+ .unwrap_err();
+
+ assert!(format!("{error:#}").contains("transaction exceeds max block size"));
}
#[test]