//! Deterministic, seedable workload generation. No external RNG crate: a small //! SplitMix64 gives reproducible batches so a failing case can be replayed //! exactly. `conflict` in [0,1] tunes how often a transaction touches the small //! "hot" set (shared accounts / the shared counter / the shared AMM pool), //! which is what drives the abort rate and destroys parallelism when high. use crate::types::{Key, StateMap, TxKind, Txn, Value}; pub struct SplitMix64 { state: u64, } impl SplitMix64 { pub fn new(seed: u64) -> Self { SplitMix64 { state: seed } } pub fn next_u64(&mut self) -> u64 { self.state = self.state.wrapping_add(0x9E3779B97F4A7C15); let mut z = self.state; z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9); z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB); z ^ (z >> 31) } pub fn below(&mut self, n: u64) -> u64 { if n == 0 { 0 } else { self.next_u64() % n } } /// probability p in [0,1] scaled to 1e6. pub fn chance(&mut self, p_ppm: u64) -> bool { self.below(1_000_000) < p_ppm } } pub struct WorkloadConfig { pub num_txns: usize, pub num_accounts: u64, pub num_hot_accounts: u64, pub num_contracts: u64, pub num_hot_slots: u64, /// probability (0..1e6 ppm) that a given account/slot pick is from the hot set pub conflict_ppm: u64, /// simulated EVM execution cost per tx (keccak rounds). 0 = trivial tx. pub gas: u32, pub seed: u64, } /// Build the base state: every account funded, hot slots seeded, AMM pools seeded. pub fn make_base_state(cfg: &WorkloadConfig) -> StateMap { let mut s = StateMap::new(); for a in 0..cfg.num_accounts { s.insert(Key::Balance(a), 1_000_000u128); } for c in 0..cfg.num_contracts { // slot layout: 0..num_hot_slots = counters; then two AMM slots 1000,1001 for slot in 0..cfg.num_hot_slots { s.insert(Key::Storage(c, slot), 0); } s.insert(Key::Storage(c, 1000), 1_000_000u128); // AMM reserve x s.insert(Key::Storage(c, 1001), 1_000_000u128); // AMM reserve y } s } impl WorkloadConfig { fn pick_account(&self, rng: &mut SplitMix64) -> u64 { if self.num_hot_accounts > 0 && rng.chance(self.conflict_ppm) { rng.below(self.num_hot_accounts) } else { self.num_hot_accounts + rng.below(self.num_accounts - self.num_hot_accounts) } } } /// Generate a batch. Mix: transfers, sweeps, increments, AMM swaps. pub fn generate_batch(cfg: &WorkloadConfig) -> Vec { let mut rng = SplitMix64::new(cfg.seed); let mut txns = Vec::with_capacity(cfg.num_txns); for _ in 0..cfg.num_txns { let roll = rng.below(100); let kind = if roll < 60 { // 60% transfers let from = cfg.pick_account(&mut rng); let mut to = cfg.pick_account(&mut rng); if to == from { to = (to + 1) % cfg.num_accounts; } let amount = 1 + rng.below(1000) as Value; TxKind::Transfer { from, to, amount } } else if roll < 75 { // 15% sweeps let from = cfg.pick_account(&mut rng); let mut to = cfg.pick_account(&mut rng); if to == from { to = (to + 1) % cfg.num_accounts; } TxKind::Sweep { from, to } } else if roll < 90 { // 15% shared-counter increments (the serializability stressor) let contract = rng.below(cfg.num_contracts.max(1)); let slot = if cfg.num_hot_slots > 0 { rng.below(cfg.num_hot_slots) } else { 0 }; TxKind::Increment { contract, slot } } else { // 10% AMM swaps against a shared pool let contract = rng.below(cfg.num_contracts.max(1)); let dx = 1 + rng.below(10_000) as Value; TxKind::AmmSwap { contract, x_slot: 1000, y_slot: 1001, dx, } }; txns.push(Txn { kind, gas: cfg.gas }); } txns }