use alloy_consensus::{transaction::Recovered, Signed, TxLegacy};
use alloy_hardforks::ForkCondition;
use alloy_primitives::{address, Address, Bytes, Signature, TxKind, U256};
use mega_evm::{
constants, hardfork_schedule, mainnet_hardforks, testnet_hardforks, BlockLimits,
EnrichedMegaTx, MegaHardfork, MegaHardforkConfig, MegaHardforks, MegaTransactionExt,
MegaTxEnvelope, MAINNET_CHAIN_ID, TESTNET_CHAIN_ID,
};
const CALLER: Address = address!("2000000000000000000000000000000000000001");
const CONTRACT: Address = address!("3000000000000000000000000000000000000001");
const STORED_TX_HASH: alloy_primitives::B256 =
alloy_primitives::b256!("00000000000000000000000000000000000000000000000000000000000000aa");
fn legacy_envelope() -> MegaTxEnvelope {
let tx = TxLegacy {
chain_id: Some(1),
nonce: 7,
gas_price: 9,
gas_limit: 21_000,
to: TxKind::Call(CONTRACT),
value: U256::from(11),
input: Bytes::from_static(&[0x12, 0x34, 0x56, 0x78, 0xaa]),
};
MegaTxEnvelope::Legacy(Signed::new_unchecked(tx, Signature::test_signature(), STORED_TX_HASH))
}
#[test]
fn test_mega_tx_envelope_ext_returns_real_size_da_and_hash() {
let tx = legacy_envelope();
let tx_size = MegaTransactionExt::tx_size(&tx);
let da_size = MegaTransactionExt::estimated_da_size(&tx);
assert!(tx_size > 1, "tx_size collapsed to a constant: {tx_size}");
assert!(da_size > 1, "estimated_da_size collapsed to a constant: {da_size}");
let h = MegaTransactionExt::tx_hash(&tx);
assert_ne!(h, alloy_primitives::TxHash::ZERO, "tx_hash returned Default::default()");
assert_eq!(h, STORED_TX_HASH, "tx_hash mismatch");
}
#[test]
fn test_recovered_mega_tx_envelope_ext_returns_real_hash() {
let tx = legacy_envelope();
let recovered = Recovered::new_unchecked(tx, CALLER);
let h = MegaTransactionExt::tx_hash(&recovered);
assert_ne!(h, alloy_primitives::TxHash::ZERO, "recovered tx_hash returned Default::default()");
assert_eq!(h, STORED_TX_HASH, "recovered tx_hash mismatch");
}
#[test]
fn test_recovered_ref_mega_tx_envelope_ext_returns_real_hash() {
let tx = legacy_envelope();
let recovered = Recovered::new_unchecked(&tx, CALLER);
let h = MegaTransactionExt::tx_hash(&recovered);
assert_ne!(
h,
alloy_primitives::TxHash::ZERO,
"recovered-ref tx_hash returned Default::default()"
);
assert_eq!(h, STORED_TX_HASH, "recovered-ref tx_hash mismatch");
}
#[test]
fn test_enriched_mega_tx_ext_returns_stored_fields_not_recomputed() {
let tx = legacy_envelope();
let recovered = Recovered::new_unchecked(tx, CALLER);
let real_da_size = MegaTransactionExt::estimated_da_size(&recovered);
let real_tx_size = MegaTransactionExt::tx_size(&recovered);
const STORED_DA_SIZE: u64 = 424_242;
const STORED_TX_SIZE: u64 = 131_313;
assert_ne!(STORED_DA_SIZE, real_da_size, "stored da_size must differ from a fresh recompute");
assert_ne!(STORED_TX_SIZE, real_tx_size, "stored tx_size must differ from a fresh recompute");
let enriched = EnrichedMegaTx::new(recovered, STORED_TX_HASH, STORED_DA_SIZE, STORED_TX_SIZE);
assert_eq!(
MegaTransactionExt::estimated_da_size(&enriched),
STORED_DA_SIZE,
"estimated_da_size must return the stored field, not a recomputed value"
);
assert_eq!(
MegaTransactionExt::tx_size(&enriched),
STORED_TX_SIZE,
"tx_size must return the stored field, not a recomputed value"
);
assert_eq!(
MegaTransactionExt::tx_hash(&enriched),
STORED_TX_HASH,
"tx_hash must return the stored field, not Default::default()"
);
}
#[test]
fn test_block_limits_from_rex_sets_all_three_block_fields() {
let limits = BlockLimits::from_hardfork_and_block_gas_limit(MegaHardfork::Rex4, 50_000_000);
assert_eq!(
limits.block_txs_data_limit,
constants::mini_rex::BLOCK_DATA_LIMIT,
"block_txs_data_limit initializer dropped"
);
assert_eq!(
limits.block_kv_update_limit,
constants::mini_rex::BLOCK_KV_UPDATE_LIMIT,
"block_kv_update_limit initializer dropped"
);
assert_eq!(
limits.block_state_growth_limit,
constants::rex::BLOCK_STATE_GROWTH_LIMIT,
"block_state_growth_limit initializer dropped"
);
assert_eq!(limits.block_gas_limit, 50_000_000, "block gas limit not applied");
}
#[test]
fn test_block_limits_from_mini_rex_sets_data_and_kv_fields() {
let limits = BlockLimits::from_hardfork_and_block_gas_limit(MegaHardfork::MiniRex, 30_000_000);
assert_eq!(
limits.block_txs_data_limit,
constants::mini_rex::BLOCK_DATA_LIMIT,
"MiniRex block_txs_data_limit initializer dropped"
);
assert_eq!(
limits.block_kv_update_limit,
constants::mini_rex::BLOCK_KV_UPDATE_LIMIT,
"MiniRex block_kv_update_limit initializer dropped"
);
assert_eq!(
limits.block_state_growth_limit,
u64::MAX,
"MiniRex must not set a block state-growth limit"
);
}
fn staged_config() -> MegaHardforkConfig {
MegaHardforkConfig::new()
.with(MegaHardfork::MiniRex, ForkCondition::Timestamp(100))
.with(MegaHardfork::MiniRex1, ForkCondition::Timestamp(200))
.with(MegaHardfork::MiniRex2, ForkCondition::Timestamp(300))
.with(MegaHardfork::Rex, ForkCondition::Timestamp(400))
.with(MegaHardfork::Rex1, ForkCondition::Timestamp(500))
.with(MegaHardfork::Rex2, ForkCondition::Timestamp(600))
.with(MegaHardfork::Rex3, ForkCondition::Timestamp(700))
.with(MegaHardfork::Rex4, ForkCondition::Timestamp(800))
.with(MegaHardfork::Rex5, ForkCondition::Timestamp(900))
}
#[test]
fn test_hardfork_activation_predicates_are_true_at_activation() {
let cfg = staged_config();
assert!(!cfg.is_mini_rex_1_active_at_timestamp(199));
assert!(cfg.is_mini_rex_1_active_at_timestamp(200));
assert!(!cfg.is_mini_rex_2_active_at_timestamp(299));
assert!(cfg.is_mini_rex_2_active_at_timestamp(300));
assert!(!cfg.is_rex_1_active_at_timestamp(499));
assert!(cfg.is_rex_1_active_at_timestamp(500));
assert!(!cfg.is_rex_3_active_at_timestamp(699));
assert!(cfg.is_rex_3_active_at_timestamp(700));
assert!(cfg.is_mini_rex_active_at_timestamp(100));
assert!(cfg.is_rex_active_at_timestamp(400));
assert!(cfg.is_rex_2_active_at_timestamp(600));
assert!(cfg.is_rex_4_active_at_timestamp(800));
assert!(cfg.is_rex_5_active_at_timestamp(900));
}
#[test]
fn test_hardfork_schedule_mainnet_arm_returns_mainnet_schedule() {
let from_dispatch = hardfork_schedule(MAINNET_CHAIN_ID);
let expected = mainnet_hardforks();
assert_eq!(
from_dispatch.get(MegaHardfork::MiniRex1),
expected.get(MegaHardfork::MiniRex1),
"mainnet arm did not return the mainnet schedule"
);
assert_eq!(
from_dispatch.get(MegaHardfork::MiniRex1),
Some(&ForkCondition::Timestamp(1764845637)),
"mainnet MiniRex1 activation timestamp changed"
);
assert_eq!(
from_dispatch.get(MegaHardfork::Rex5),
Some(&ForkCondition::Timestamp(1780632000)),
"mainnet Rex5 activation timestamp changed"
);
}
#[test]
fn test_hardfork_schedule_testnet_arm_returns_testnet_schedule() {
let from_dispatch = hardfork_schedule(TESTNET_CHAIN_ID);
let expected = testnet_hardforks();
assert_eq!(
from_dispatch.get(MegaHardfork::MiniRex1),
expected.get(MegaHardfork::MiniRex1),
"testnet arm did not return the testnet schedule"
);
assert_eq!(
from_dispatch.get(MegaHardfork::MiniRex1),
Some(&ForkCondition::Never),
"testnet MiniRex1 should be Never"
);
assert_eq!(
from_dispatch.get(MegaHardfork::Rex5),
Some(&ForkCondition::Timestamp(1780459200)),
"testnet Rex5 activation timestamp changed"
);
}
use alloy_consensus::transaction::Recovered as MegaRecovered;
use alloy_primitives::B256;
use mega_evm::{BlockLimiter, BlockMegaTransactionOutcome, MegaHaltReason, MegaTransactionOutcome};
use revm::{
context::result::{ExecutionResult, Output, SuccessReason},
state::EvmState,
};
fn outcome_for(
tx: MegaRecovered<MegaTxEnvelope>,
da_size: u64,
) -> BlockMegaTransactionOutcome<MegaRecovered<MegaTxEnvelope>> {
BlockMegaTransactionOutcome {
tx,
tx_size: 0,
da_size,
depositor: None,
inner: MegaTransactionOutcome {
result: ExecutionResult::<MegaHaltReason>::Success {
reason: SuccessReason::Stop,
gas_used: 0,
gas_refunded: 0,
logs: Vec::new(),
output: Output::Call(Bytes::new()),
},
state: EvmState::default(),
data_size: 0,
kv_updates: 0,
compute_gas_used: 0,
state_growth_used: 0,
},
}
}
fn legacy_recovered() -> MegaRecovered<MegaTxEnvelope> {
let tx = TxLegacy {
chain_id: Some(1),
nonce: 0,
gas_price: 1,
gas_limit: 21_000,
to: TxKind::Call(CONTRACT),
value: U256::ZERO,
input: Bytes::new(),
};
let signed = Signed::new_unchecked(tx, Signature::test_signature(), B256::ZERO);
MegaRecovered::new_unchecked(MegaTxEnvelope::Legacy(signed), CALLER)
}
#[test]
fn test_pre_execution_check_tx_gas_limit_boundary() {
let mut limits = BlockLimits::no_limits();
limits.tx_gas_limit = 1_000;
let limiter = BlockLimiter::new(limits);
assert!(
limiter.pre_execution_check(B256::ZERO, 1_000, 0, 0, false).is_ok(),
"gas_limit == tx_gas_limit must be admitted (boundary is strict `>`)"
);
let over = limiter.pre_execution_check(B256::ZERO, 1_001, 0, 0, false);
assert!(over.is_err(), "gas_limit == tx_gas_limit + 1 must be rejected");
assert!(
format!("{:?}", over.unwrap_err()).contains("TransactionGasLimit"),
"over-limit gas must report the per-tx gas-limit error"
);
}
#[test]
fn test_pre_execution_check_block_gas_boundary() {
let mut limits = BlockLimits::no_limits();
limits.block_gas_limit = 1_000;
let limiter = BlockLimiter::new(limits);
assert!(
limiter.pre_execution_check(B256::ZERO, 1_000, 0, 0, false).is_ok(),
"block_gas_used + gas_limit == block_gas_limit must be admitted"
);
let over = limiter.pre_execution_check(B256::ZERO, 1_001, 0, 0, false);
assert!(over.is_err(), "block_gas_used + gas_limit == block_gas_limit + 1 must be rejected");
assert!(
format!("{:?}", over.unwrap_err()).contains("TransactionGasLimitMoreThanAvailableBlockGas"),
"over-limit block gas must report the available-block-gas error"
);
}
#[test]
fn test_pre_execution_check_tx_size_boundary() {
let mut limits = BlockLimits::no_limits();
limits.tx_encode_size_limit = 500;
let limiter = BlockLimiter::new(limits);
assert!(
limiter.pre_execution_check(B256::ZERO, 0, 500, 0, false).is_ok(),
"tx_size == tx_encode_size_limit must be admitted"
);
let over = limiter.pre_execution_check(B256::ZERO, 0, 501, 0, false);
assert!(over.is_err(), "tx_size == tx_encode_size_limit + 1 must be rejected");
assert!(
format!("{:?}", over.unwrap_err()).contains("TransactionEncodeSizeLimit"),
"over-limit tx size must report the per-tx encode-size error"
);
}
#[test]
fn test_pre_execution_check_block_da_size_boundary() {
let mut limits = BlockLimits::no_limits();
limits.block_da_size_limit = 800;
let limiter = BlockLimiter::new(limits);
assert!(
limiter.pre_execution_check(B256::ZERO, 0, 0, 800, false).is_ok(),
"da_size + block_da_size_used == block_da_size_limit must be admitted"
);
let over = limiter.pre_execution_check(B256::ZERO, 0, 0, 801, false);
assert!(
over.is_err(),
"da_size + block_da_size_used == block_da_size_limit + 1 must be rejected"
);
assert!(
format!("{:?}", over.unwrap_err()).contains("DataAvailabilitySizeLimit"),
"over-limit block da must report the data-availability error"
);
}
#[test]
fn test_post_execution_update_advances_da_for_non_deposit() {
let mut limiter = BlockLimiter::new(BlockLimits::no_limits());
let outcome = outcome_for(legacy_recovered(), 1_234);
limiter.post_execution_update(&outcome).expect("post_execution_update should succeed");
assert_eq!(
limiter.block_da_size_used, 1_234,
"a non-deposit tx must accumulate da_size; the `==`→`!=` mutant would skip it (0)"
);
}
#[test]
fn test_is_block_limit_reached_all_below_is_false() {
let mut limits = BlockLimits::no_limits();
limits.block_gas_limit = 10;
limits.block_txs_encode_size_limit = 10;
limits.block_da_size_limit = 10;
limits.block_txs_data_limit = 10;
limits.block_kv_update_limit = 10;
limits.block_compute_gas_limit = 10;
limits.block_state_growth_limit = 10;
let mut limiter = BlockLimiter::new(limits);
limiter.block_gas_used = 9;
limiter.block_tx_size_used = 9;
limiter.block_da_size_used = 9;
limiter.block_data_used = 9;
limiter.block_kv_updates_used = 9;
limiter.block_compute_gas_used = 9;
limiter.block_state_growth_used = 9;
assert!(
!limiter.is_block_limit_reached(),
"with every counter strictly below its limit the block is not full"
);
}
macro_rules! only_dimension_at_limit {
($limit_field:ident, $used_field:ident) => {{
let mut limits = BlockLimits::no_limits();
limits.block_gas_limit = 10;
limits.block_txs_encode_size_limit = 10;
limits.block_da_size_limit = 10;
limits.block_txs_data_limit = 10;
limits.block_kv_update_limit = 10;
limits.block_compute_gas_limit = 10;
limits.block_state_growth_limit = 10;
limits.$limit_field = 5;
let mut limiter = BlockLimiter::new(limits);
limiter.block_gas_used = 0;
limiter.block_tx_size_used = 0;
limiter.block_da_size_used = 0;
limiter.block_data_used = 0;
limiter.block_kv_updates_used = 0;
limiter.block_compute_gas_used = 0;
limiter.block_state_growth_used = 0;
limiter.$used_field = 5;
limiter
}};
}
#[test]
fn test_is_block_limit_reached_gas_dimension() {
let limiter = only_dimension_at_limit!(block_gas_limit, block_gas_used);
assert!(limiter.is_block_limit_reached(), "gas at limit ⇒ block full");
}
#[test]
fn test_is_block_limit_reached_tx_size_dimension() {
let limiter = only_dimension_at_limit!(block_txs_encode_size_limit, block_tx_size_used);
assert!(limiter.is_block_limit_reached(), "tx encode size at limit ⇒ block full");
}
#[test]
fn test_is_block_limit_reached_da_size_dimension() {
let limiter = only_dimension_at_limit!(block_da_size_limit, block_da_size_used);
assert!(limiter.is_block_limit_reached(), "da size at limit ⇒ block full");
}
#[test]
fn test_is_block_limit_reached_data_dimension() {
let limiter = only_dimension_at_limit!(block_txs_data_limit, block_data_used);
assert!(limiter.is_block_limit_reached(), "tx data at limit ⇒ block full");
}
#[test]
fn test_is_block_limit_reached_kv_updates_dimension() {
let limiter = only_dimension_at_limit!(block_kv_update_limit, block_kv_updates_used);
assert!(limiter.is_block_limit_reached(), "kv updates at limit ⇒ block full");
}
#[test]
fn test_is_block_limit_reached_compute_gas_dimension() {
let limiter = only_dimension_at_limit!(block_compute_gas_limit, block_compute_gas_used);
assert!(limiter.is_block_limit_reached(), "compute gas at limit ⇒ block full");
}
#[test]
fn test_is_block_limit_reached_state_growth_dimension() {
let limiter = only_dimension_at_limit!(block_state_growth_limit, block_state_growth_used);
assert!(limiter.is_block_limit_reached(), "state growth at limit ⇒ block full");
}