commit de3f62279a3e571d8cd1a89d90d4ad3050919d26
parent cae387c48f6f2caec2757a2e2cd7d25f3cf8d203
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Tue, 15 Sep 2026 08:42:15 +0200
test(protocol): run regular block-budget coverage post-activation
Diffstat:
2 files changed, 170 insertions(+), 13 deletions(-)
diff --git a/src/domain/adversarial_tests.rs b/src/domain/adversarial_tests.rs
@@ -14,9 +14,9 @@ use super::{
BurnBundleSignature, BurnCommitteeMember, BurnLeaderRank, ChainSnapshot, FinalizerMode,
GRINDING_RESISTANCE_ACTIVATION_HEIGHT, GenesisBurn, LeaderProofPayload, Ledger,
MAX_BLOCK_BYTES, MAX_BURN_BUNDLE_BYTES, MICRO_IUNA, MaskedBurn,
- OBJECTIVE_FINALITY_ACTIVATION_HEIGHT, OutPoint, Transaction, TransactionSubmitOutcome,
- TxOutput, UtxoLineageRoot, VDF_TARGET_BLOCK_MS, Wallet, genesis_allocation_outpoint, hex_hash,
- reward_outputs_for_block, run_vdf,
+ OBJECTIVE_FINALITY_ACTIVATION_HEIGHT, OutPoint, TRANSACTION_V2_ACTIVATION_HEIGHT, Transaction,
+ TransactionSubmitOutcome, TxOutput, UtxoLineageRoot, VDF_TARGET_BLOCK_MS, Wallet,
+ genesis_allocation_outpoint, hex_hash, reward_outputs_for_block, run_vdf,
};
const NOW_MS: u64 = 10_000_000_000;
@@ -214,6 +214,14 @@ impl Harness {
.unwrap_or_else(|| panic!("seed {} missing wallet {address}", self.seed))
}
+ fn set_next_height(&mut self, next_height: u64) {
+ self.ledger.chain.last_mut().unwrap().height = next_height.saturating_sub(1);
+ for ticket in &mut self.ledger.tickets {
+ ticket.eligible_from_height = next_height;
+ ticket.eligible_until_height = next_height;
+ }
+ }
+
fn next_rank(&self, rank: usize) -> BurnLeaderRank {
let height = self.ledger.height() + 1;
let ticket = self
@@ -4341,9 +4349,14 @@ fn attack_economics_committee_capture_requires_matured_lineage_weight() {
#[test]
fn performance_budget_block_validation_rejects_count_and_byte_overflow() {
let mut harness = harness_for_percent(1_500, 25);
+ harness.set_next_height(TRANSACTION_V2_ACTIVATION_HEIGHT.unwrap().saturating_add(1));
let leader = harness.next_rank(0);
let finalizer = harness.wallet(&leader.owner).clone();
- harness.submit_anchor_burn(&finalizer);
+ let anchor = harness
+ .ledger
+ .build_burn_for_next_block(&finalizer, 1, 1)
+ .unwrap();
+ harness.ledger.submit_transaction(anchor).unwrap();
let block = harness.finish_ticket_block_from_pending(0, Vec::new());
let now_ms = NOW_MS.saturating_add(block.timestamp_ms);
@@ -4466,11 +4479,14 @@ fn blockspace_flood_stays_bounded_by_transaction_count_and_bytes() {
let senders = (0..(max_test_transactions + 32))
.map(|index| Wallet::from_seed(&format!("blockspace-flood-sender-{index}")))
.collect::<Vec<_>>();
+ let v2_migrators = (0..32)
+ .map(|index| Wallet::from_seed(&format!("blockspace-flood-v2-migrator-{index}")))
+ .collect::<Vec<_>>();
let mut allocations = BTreeMap::new();
allocations.insert(finalizer.address().to_string(), 10 * MICRO_IUNA);
allocations.insert(recipient.address().to_string(), 10 * MICRO_IUNA);
- for sender in &senders {
- allocations.insert(sender.address().to_string(), 10 * MICRO_IUNA);
+ for wallet in senders.iter().chain(&v2_migrators) {
+ allocations.insert(wallet.address().to_string(), 10 * MICRO_IUNA);
}
let mut ledger = Ledger::new_with_genesis_burns(
allocations,
@@ -4478,6 +4494,12 @@ fn blockspace_flood_stays_bounded_by_transaction_count_and_bytes() {
1,
)
.unwrap();
+ let next_height = TRANSACTION_V2_ACTIVATION_HEIGHT.unwrap().saturating_add(1);
+ ledger.chain.last_mut().unwrap().height = next_height.saturating_sub(1);
+ for ticket in &mut ledger.tickets {
+ ticket.eligible_from_height = next_height;
+ ticket.eligible_until_height = next_height;
+ }
ledger.launch_profile.max_block_transactions = max_test_transactions;
for sender in &senders {
@@ -4486,7 +4508,11 @@ fn blockspace_flood_stays_bounded_by_transaction_count_and_bytes() {
.unwrap();
ledger.submit_transaction(tx).unwrap();
}
- let anchor = ledger.build_burn(&finalizer, 1, 1).unwrap();
+ for migrator in &v2_migrators {
+ let transaction = ledger.build_v2_migration(migrator, 10).unwrap();
+ ledger.submit_transaction_v2(transaction).unwrap();
+ }
+ let anchor = ledger.build_burn_for_next_block(&finalizer, 1, 1).unwrap();
ledger.submit_transaction(anchor.clone()).unwrap();
let prepared = ledger
@@ -4502,11 +4528,11 @@ fn blockspace_flood_stays_bounded_by_transaction_count_and_bytes() {
.any(|tx| tx.signature() == anchor.signature()),
"flooded block did not preserve the required finalizer anchor burn"
);
+ let selected_transactions = block.transactions.len() + block.transactions_v2.len();
+ assert!(!block.transactions_v2.is_empty());
assert!(
- block.transactions.len() <= max_test_transactions,
- "block selected too many transactions: {} > {}",
- block.transactions.len(),
- max_test_transactions
+ selected_transactions <= max_test_transactions,
+ "block selected too many transactions: {selected_transactions} > {max_test_transactions}"
);
let block_bytes = ledger.consensus_block_size_bytes(&block).unwrap();
assert!(
@@ -4518,7 +4544,7 @@ fn blockspace_flood_stays_bounded_by_transaction_count_and_bytes() {
.apply_block_at(block, NOW_MS.saturating_add(VDF_TARGET_BLOCK_MS))
.unwrap();
assert!(
- !ledger.pending().is_empty(),
+ !ledger.pending().is_empty() || !ledger.pending_v2().is_empty(),
"blockspace flood should leave excess paid transactions pending instead of exceeding limits"
);
}
diff --git a/src/domain/ledger_v2.rs b/src/domain/ledger_v2.rs
@@ -454,7 +454,22 @@ mod tests {
use std::collections::BTreeMap;
use super::*;
- use crate::domain::{GenesisBurn, LaunchProfile, SignatureScheme, TransactionV2Output, Wallet};
+ use crate::domain::{
+ GenesisBurn, LaunchProfile, SignatureScheme, TRANSACTION_V2_ACTIVATION_HEIGHT,
+ TransactionV2Output, Wallet,
+ };
+
+ fn set_next_height(ledger: &mut Ledger, next_height: u64) {
+ ledger.chain.last_mut().unwrap().height = next_height.saturating_sub(1);
+ for ticket in &mut ledger.tickets {
+ ticket.eligible_from_height = next_height;
+ ticket.eligible_until_height = next_height;
+ }
+ }
+
+ fn post_activation_height() -> u64 {
+ TRANSACTION_V2_ACTIVATION_HEIGHT.unwrap().saturating_add(1)
+ }
#[test]
fn migration_validation_switches_at_3000_and_creates_a_hybrid_utxo() {
@@ -821,4 +836,120 @@ mod tests {
);
assert!(ledger.pending_v2().is_empty());
}
+
+ #[test]
+ fn normal_post_activation_block_flow_shares_the_transaction_count_budget() {
+ let finalizer = Wallet::from_seed("v2-post-activation-count-finalizer");
+ let recipient = Wallet::from_seed("v2-post-activation-count-recipient");
+ let migrators = (0..4)
+ .map(|index| Wallet::from_seed(&format!("v2-post-activation-count-{index}")))
+ .collect::<Vec<_>>();
+ let legacy_senders = (0..2)
+ .map(|index| Wallet::from_seed(&format!("legacy-post-activation-count-{index}")))
+ .collect::<Vec<_>>();
+ let mut allocations = BTreeMap::from([
+ (finalizer.address().to_string(), 100),
+ (recipient.address().to_string(), 100),
+ ]);
+ for wallet in migrators.iter().chain(&legacy_senders) {
+ allocations.insert(wallet.address().to_string(), 100);
+ }
+ let mut ledger = Ledger::new_with_genesis_burns_and_profile(
+ allocations,
+ vec![GenesisBurn::new(finalizer.address(), 10)],
+ 1,
+ LaunchProfile::local_testnet(),
+ )
+ .unwrap();
+ let next_height = post_activation_height();
+ set_next_height(&mut ledger, next_height);
+ ledger.launch_profile.max_block_transactions = 4;
+
+ let anchor = ledger.build_burn_for_next_block(&finalizer, 1, 1).unwrap();
+ ledger.submit_transaction(anchor.clone()).unwrap();
+ for (index, wallet) in migrators.iter().enumerate() {
+ let migration = ledger
+ .build_v2_migration(wallet, 10 + index as u64)
+ .unwrap();
+ ledger.submit_transaction_v2(migration).unwrap();
+ }
+ for wallet in &legacy_senders {
+ let transfer = ledger
+ .build_transfer(wallet, recipient.address(), 10, 1)
+ .unwrap();
+ ledger.submit_transaction(transfer).unwrap();
+ }
+
+ let prepared = ledger
+ .prepare_next_block(
+ finalizer.address(),
+ ledger.tip().timestamp_ms.saturating_add(1),
+ )
+ .unwrap();
+ assert_eq!(
+ prepared.transactions.len() + prepared.transactions_v2.len(),
+ ledger.launch_profile.max_block_transactions
+ );
+ assert!(
+ prepared
+ .transactions
+ .iter()
+ .any(|transaction| transaction.signature() == anchor.signature())
+ );
+ assert!(!prepared.transactions_v2.is_empty());
+ let block = prepared.finish(&finalizer, "preverified-vdf".to_string());
+ ledger.apply_preverified_block_at(block, u64::MAX).unwrap();
+
+ assert_eq!(ledger.height(), next_height);
+ assert!(
+ ledger.pending().len() + ledger.pending_v2().len() > 0,
+ "transactions over the shared block limit must remain pending"
+ );
+ }
+
+ #[test]
+ fn normal_post_activation_v2_block_enforces_the_exact_byte_boundary() {
+ let finalizer = Wallet::from_seed("v2-post-activation-bytes-finalizer");
+ let migrator = Wallet::from_seed("v2-post-activation-bytes-migrator");
+ let mut ledger = Ledger::new_with_genesis_burns_and_profile(
+ BTreeMap::from([
+ (finalizer.address().to_string(), 100),
+ (migrator.address().to_string(), 100),
+ ]),
+ vec![GenesisBurn::new(finalizer.address(), 10)],
+ 1,
+ LaunchProfile::local_testnet(),
+ )
+ .unwrap();
+ let next_height = post_activation_height();
+ set_next_height(&mut ledger, next_height);
+ let anchor = ledger.build_burn_for_next_block(&finalizer, 1, 1).unwrap();
+ ledger.submit_transaction(anchor).unwrap();
+ let migration = ledger.build_v2_migration(&migrator, 3).unwrap();
+ ledger.submit_transaction_v2(migration).unwrap();
+ let prepared = ledger
+ .prepare_next_block(
+ finalizer.address(),
+ ledger.tip().timestamp_ms.saturating_add(1),
+ )
+ .unwrap();
+ assert_eq!(prepared.transactions_v2.len(), 1);
+ let block = prepared.finish(&finalizer, "preverified-vdf".to_string());
+ let block_bytes = ledger.consensus_block_size_bytes(&block).unwrap();
+
+ let mut exact = ledger.clone();
+ exact.launch_profile.max_block_bytes = block_bytes;
+ exact
+ .apply_preverified_block_at(block.clone(), u64::MAX)
+ .expect("post-activation v2 block at the byte limit should validate");
+
+ let mut one_byte_over = ledger;
+ one_byte_over.launch_profile.max_block_bytes = block_bytes.saturating_sub(1);
+ assert!(
+ one_byte_over
+ .apply_preverified_block_at(block, u64::MAX)
+ .is_err(),
+ "post-activation v2 block one byte over the limit validated"
+ );
+ }
}