A Hands-On MEV Simulation in Python
Every time a new block is about to be built, a miner (or in post-Merge Ethereum, a block builder/proposer) faces a surprisingly rich optimization problem hiding behind a simple-looking mempool: which pending transactions do I include, and in what order, to squeeze out the maximum possible revenue?
This isn’t just “sort by gas price and go.” Real mempools contain nonce-ordering constraints (a sender’s transactions must be included in nonce order), hard gas limits per block, and — most interestingly — MEV (Maximal Extractable Value) opportunities such as sandwich attacks, where a searcher’s profit only materializes if a frontrun transaction and a backrun transaction are placed immediately before and after a victim’s trade. Get the order wrong, and that extra profit evaporates even if all three transactions are technically included.
In this article we’ll build a realistic synthetic mempool, formalize the revenue-maximization problem mathematically, show why brute-force search collapses under combinatorial explosion, and then implement a fast, exact dynamic programming solution that a real block builder could actually run in production. We’ll wrap it up with several visualizations, including a 3D revenue landscape.
1. The Problem, Formally
Let $T = {1, 2, \dots, n}$ be the set of pending transactions in the mempool. Each transaction $i$ has a gas price $p_i$, gas usage $g_i$, and base fee revenue $r_i = p_i \cdot g_i$.
Let $D$ be the set of precedence constraints coming from per-sender nonce ordering: $(i,j) \in D$ means transaction $i$ must appear strictly before $j$ in the final block ordering $\pi$.
Let $B$ be the set of MEV “sandwich” opportunities. Each opportunity $k \in B$ consists of a frontrun transaction $f_k$, a victim transaction $v_k$, and a backrun transaction $b_k$, together with a bonus $\beta_k$ that is only earned if all three are included and placed in strict adjacency.
The block builder chooses a subset $S \subseteq T$ and an ordering $\pi$ to maximize:
$$
\max_{S \subseteq T,\ \pi} \quad R(S,\pi) = \sum_{i \in S} r_i ;+; \sum_{k \in B} \beta_k \cdot \mathbb{1}\Big[{f_k, v_k, b_k} \subseteq S \ \wedge\ \pi(v_k) = \pi(f_k)+1 \ \wedge\ \pi(b_k) = \pi(v_k)+1\Big]
$$
subject to:
$$
\sum_{i \in S} g_i \le G_{\text{block}}, \qquad \pi(i) < \pi(j) \ \ \forall (i,j) \in D
$$
This is a knapsack problem (gas-limited selection) coupled with a scheduling problem (precedence + adjacency), which in its general form is NP-hard. The trick we’ll use below is to notice that both the nonce chains and the sandwich bundles can be pre-packaged into “atomic units,” turning the selection stage into a clean multiple-choice knapsack that’s solvable exactly and quickly with DP, while the ordering stage reduces to a simple, provably-safe greedy pass.
2. Why Brute Force Fails
If a builder naively tried every subset of transactions combined with every valid ordering, the search space would explode combinatorially — even 40–50 competing transaction groups (senders + MEV bundles) can produce a search space in the order of $10^{20}$ or more combinations, something no amount of compute can brute-force within a 12-second block time. We’ll actually demonstrate this blow-up numerically in the code below, on a small subset first (to validate correctness against the DP), and then print the true combinatorial size of the full problem to show why brute force is a non-starter at scale.
3. The Optimized Algorithm
The core trick: group transactions into decision units:
- Nonce chains — for each sender, create “prefix” options: include the first $k$ of their queued transactions ($k = 0, 1, \dots$), since nonce ordering means you can never skip ahead.
- Sandwich bundles — for each MEV opportunity, create exactly 3 options: skip it, include only the victim transaction, or include the full front–victim–back bundle (gas and revenue summed, plus the bonus).
Each group contributes exactly one chosen option to the final block. This is precisely a multiple-choice knapsack problem, solved by the recurrence:
$$
dp[c] = \max_{o ,\in, O_g} \Big( dp_{\text{prev}}[,c - g_o,] + r_o \Big), \qquad c \ge g_o
$$
where $O_g$ is the set of options for group $g$, $g_o$ is the gas cost of option $o$, and $r_o$ its revenue (bonus included where relevant). This DP runs in $O(,|\text{groups}| \times |\text{options}| \times G_{\text{block}},)$ time — polynomial, and fast in practice.
Once the optimal selection is known, the ordering step is simple: since revenue only depends on order through the (already-guaranteed) bundle adjacency and nonce precedence — both preserved automatically because we treat chains and bundles as atomic, internally-ordered units — we just sort the chosen units by fee-density $r_{\text{unit}}/g_{\text{unit}}$ descending, mimicking a standard priority-gas-auction ordering.
4. Full Source Code
1 | import numpy as np |
5. Code Walkthrough
Section 1 — generate_mempool: builds a synthetic but structurally realistic mempool. Each of the 45 “senders” has a nonce chain of 1–3 transactions (gas price drawn from an exponential distribution to mimic the long tail of real gas markets). Six sandwich opportunities are carved out from single-transaction senders with sizable gas usage — these represent large swaps that are attractive sandwich targets. A synthetic frontrun (priced $+1$ above the victim) and backrun (priced $-1$ below) are generated for each.
Section 2 — build_groups: converts the raw mempool into multiple-choice knapsack groups. Every sender chain becomes a group of “prefix” options (include 0, 1, 2, … of their queued transactions, in order). Every sandwich becomes a 3-option group: skip, victim-only, or full-bundle-with-bonus. Note the victim’s original single-transaction chain is explicitly excluded from the general chain loop to avoid double-counting.
Section 3 — knapsack_multiple_choice: the heart of the algorithm. For each group, it vectorizes the option evaluation over the entire capacity axis using NumPy slicing instead of a triple nested Python loop — this is what keeps it fast even though we’re solving 50+ groups × up to 4 options × 2,500 gas-capacity buckets. The choice_hist list records, for every group and every capacity level, which option was chosen, enabling exact backtracking to reconstruct the optimal selection afterward.
Section 4 — naive_priority_selection: models what a “dumb” fee-priority miner does — repeatedly grab the highest gas-price transaction whose sender’s nonce is currently unlocked and that still fits in the remaining gas budget. This deliberately has no concept of bundles, so any MEV bonus is left on the table even if all three legs happen to be included.
Section 5 — build_final_order: since our decision units (chains, bundles) already encode a fixed internal order, all that’s left is deciding the relative order between units. Sorting by fee-density (revenue per unit of gas) reproduces standard priority-gas-auction behavior while guaranteeing every constraint stays satisfied.
Section 6 — brute-force validation: exhaustively enumerates every combination of options across a handful of groups via itertools.product, confirming the DP finds the identical optimum, then prints the size of the full combinatorial space to make the scaling problem concrete.
Sections 7–11: run the DP on the full mempool, compare it against the naive baseline, and produce four visualizations, described below.
6. Results & Visualizations
Running the cell above will print the validation check, the naive-vs-optimized revenue comparison, and the MEV bonus captured, followed by four charts.
[Validation] Brute-force best value : 8.2924 (time: 9.83 ms) [Validation] DP best value : 8.2924 (time: 0.4548 ms) [Validation] Match: True [Scale] Full mempool search space size: 1,009,714,565,912,092,213,248 combinations [Optimized DP] Full mempool optimal revenue: 119.3140 (solved in 6.320 ms) [Naive greedy] Revenue: 109.0606 Gas used: 2,492,000/2,500,000 [Optimized DP] Revenue: 119.3140 Gas used: 2,497,000/2,500,000 [Optimized DP] MEV bonus captured: 3.6726 (3 full sandwich bundles out of 6) [Result] MEV-aware optimization improves miner revenue by 9.40% over naive fee-based ordering
Chart 1 — Selected & Ordered Transactions. A bar per transaction actually included in the optimized block, colored by role (normal / frontrun / victim / backrun), in their final block position. Sandwich trios should visibly cluster together.

Chart 2 — Naive vs Optimized Revenue. A stacked bar comparing the naive fee-priority miner against the MEV-aware DP miner, with the captured bundle bonus shown as a separate stacked segment on the optimized bar — this is the “extra money on the table” that adjacency-aware scheduling unlocks.

Chart 3 — Runtime Comparison. Brute force on just 7 groups vs. the DP on the same 7 groups vs. the DP on the entire ~50-group mempool, log-scaled. The DP should be dramatically faster even while solving a far larger problem.

Chart 4 — 3D Revenue Landscape. A surface plot sweeping the block gas limit (X-axis) and an MEV bonus intensity multiplier (Y-axis) against the resulting optimal revenue (Z-axis), recomputed via the same DP across a 11×7 grid. This shows how sensitive miner revenue is to both block capacity and how lucrative MEV opportunities are in the current mempool — useful for reasoning about how priority fees and MEV compete for the same limited gas budget.

7. Real-World Caveats
This simulation captures the core combinatorial structure of the problem, but production MEV infrastructure (Flashbots, MEV-Boost, proposer-builder separation) goes further: real bundles can have complex atomicity requirements, gas price impact within an AMM changes revenue depending on what else is in the block, private order flow adds asymmetric information, and builders often run heuristic/ML-based search rather than exact DP once the mempool has thousands of transactions with dozens of interacting bundles. Still, the multiple-choice knapsack formulation shown here is a solid mental model — and a genuinely usable starting point — for reasoning about transaction selection and ordering under a gas constraint.





























