|
| 1 | +## Mempool |
| 2 | + |
| 3 | +### Glossary |
| 4 | + |
| 5 | +1. **selection session:** an ephemeral session during which the mempool selects transactions for a proposer. A session starts when a proposer asks the mempool for transactions and ends when the mempool returns the transactions. The most important part of a session is the _selection loop_. |
| 6 | +2. **transaction PPU:** the price per unit of computation, for a transaction. It's computed as `initiallyPaidFee / gasLimit`. |
| 7 | +3. **initially paid transaction fee:** the fee for processing a transaction, as known before its actual processing. That is, without knowing the _refund_ component. |
| 8 | + |
| 9 | +### Configuration |
| 10 | + |
| 11 | +1. **SelectTransactions::gasRequested:** `10_000_000_000`, the maximum total gas limit of the transactions to be returned to a proposer (one _selection session_). This value is provided by the Protocol. |
| 12 | +2. **SelectTransactions::maxNum:** `50_000`, the maximum number of transactions to be returned to a proposer (one _selection session_). This value is provided by the Protocol. |
| 13 | + |
| 14 | +### Transactions selection |
| 15 | + |
| 16 | +### Paragraph 1 |
| 17 | + |
| 18 | +When a proposer asks the mempool for transactions, it provides the following parameters: |
| 19 | + |
| 20 | + - `gasRequested`: the maximum total gas limit of the transactions to be returned |
| 21 | + - `maxNum`: the maximum number of transactions to be returned |
| 22 | + |
| 23 | +### Paragraph 2 |
| 24 | + |
| 25 | +The PPU (price per gas unit) of a transaction, is computed (once it enters the mempool) as follows: |
| 26 | + |
| 27 | +``` |
| 28 | +ppu = initiallyPaidFee / gasLimit |
| 29 | +``` |
| 30 | + |
| 31 | +In the formula above, |
| 32 | + |
| 33 | +``` |
| 34 | +initiallyPaidFee = |
| 35 | + dataCost * gasPrice + |
| 36 | + executionCost * gasPrice * network.gasPriceModifier |
| 37 | +
|
| 38 | +dataCost = network.minGasLimit + len(data) * network.gasPerDataByte |
| 39 | +
|
| 40 | +executionCost = gasLimit - dataCost |
| 41 | +``` |
| 42 | + |
| 43 | +Network parameters (as of November of 2024): |
| 44 | + |
| 45 | +``` |
| 46 | +gasPriceModifier = 0.01 |
| 47 | +minGasLimit = 50_000 |
| 48 | +gasPerDataByte = 1_500 |
| 49 | +``` |
| 50 | + |
| 51 | +#### Examples |
| 52 | + |
| 53 | +**(a)** A simple native transfer with `gasLimit = 50_000` and `gasPrice = 1_000_000_000`: |
| 54 | + |
| 55 | +``` |
| 56 | +initiallyPaidFee = 50_000_000_000 atoms |
| 57 | +ppu = 1_000_000_000 atoms |
| 58 | +``` |
| 59 | + |
| 60 | +**(b)** A simple native transfer with `gasLimit = 50_000` and `gasPrice = 1_500_000_000`: |
| 61 | + |
| 62 | +``` |
| 63 | +initiallyPaidFee = gasLimit * gasPrice = 75_000_000_000 atoms |
| 64 | +ppu = 75_000_000_000 / 50_000 = 1_500_000_000 atoms |
| 65 | +``` |
| 66 | + |
| 67 | +**(c)** A simple native transfer with a data payload of 7 bytes, with `gasLimit = 50_000 + 7 * 1500` and `gasPrice = 1_000_000_000`: |
| 68 | + |
| 69 | +``` |
| 70 | +initiallyPaidFee = 60_500_000_000_000 atoms |
| 71 | +ppu = 60_500_000_000_000 / 60_500 = 1_000_000_000 atoms |
| 72 | +``` |
| 73 | + |
| 74 | +That is, for simple native transfers (whether they hold a data payload or not), the PPU is equal to the gas price. |
| 75 | + |
| 76 | +**(d)** A contract call with `gasLimit = 75_000_000` and `gasPrice = 1_000_000_000`, with a data payload of `42` bytes: |
| 77 | + |
| 78 | +``` |
| 79 | +initiallyPaidFee = 861_870_000_000_000 atoms |
| 80 | +ppu = 11_491_600 atoms |
| 81 | +``` |
| 82 | + |
| 83 | +**(e)** Similar to **(d)**, but with `gasPrice = 2_000_000_000`: |
| 84 | + |
| 85 | +``` |
| 86 | +initiallyPaidFee = 1_723_740_000_000_000 atoms |
| 87 | +ppu = 22_983_200 atoms |
| 88 | +``` |
| 89 | + |
| 90 | +That is, for contract calls, the PPU is not equal to the gas price, but much lower, due to the contract call _cost subsidy_. **A higher gas price will result in a higher PPU.** |
| 91 | + |
| 92 | +### Paragraph 3 |
| 93 | + |
| 94 | +Transaction **A** is considered **more valuable (for the Network)** than transaction **B** if **it has a higher PPU**. |
| 95 | + |
| 96 | +If two transactions have the same PPU, they are ordered using an arbitrary, but deterministic rule: the transaction with the "lower" transaction hash "wins" the comparison. |
| 97 | + |
| 98 | +Pseudo-code: |
| 99 | + |
| 100 | +``` |
| 101 | +func isTransactionMoreValuableForNetwork(A, B): |
| 102 | + if A.ppu > B.ppu: |
| 103 | + return true |
| 104 | + if A.ppu < B.ppu: |
| 105 | + return false |
| 106 | + return A.hash < B.hash |
| 107 | +``` |
| 108 | + |
| 109 | +### Paragraph 4 |
| 110 | + |
| 111 | +The mempool selects transactions as follows (pseudo-code): |
| 112 | + |
| 113 | +``` |
| 114 | +func selectTransactions(gasRequested, maxNum): |
| 115 | + // Setup phase |
| 116 | + senders := list of all current senders in the mempool, in an arbitrary order |
| 117 | + bunchesOfTransactions := sourced from senders; nonces-gap-free, duplicates-free, nicely sorted by nonce |
| 118 | +
|
| 119 | + // Holds selected transactions |
| 120 | + selectedTransactions := empty |
| 121 | +
|
| 122 | + // Holds not-yet-selected transactions, ordered by PPU |
| 123 | + competitionHeap := empty |
| 124 | + |
| 125 | + for each bunch in bunchesOfTransactions: |
| 126 | + competitionHeap.push(next available transaction from bunch) |
| 127 | + |
| 128 | + // Selection loop |
| 129 | + while competitionHeap is not empty: |
| 130 | + mostValuableTransaction := competitionHeap.pop() |
| 131 | +
|
| 132 | + // Check if adding the next transaction exceeds limits |
| 133 | + if selectedTransactions.totalGasLimit + mostValuableTransaction.gasLimit > gasRequested: |
| 134 | + break |
| 135 | + if selectedTransactions.length + 1 > maxNum: |
| 136 | + break |
| 137 | + |
| 138 | + selectedTransactions.append(mostValuableTransaction) |
| 139 | + |
| 140 | + nextTransaction := next available transaction from the bunch of mostValuableTransaction |
| 141 | + if nextTransaction exists: |
| 142 | + competitionHeap.push(nextTransaction) |
| 143 | +
|
| 144 | + return selectedTransactions |
| 145 | +``` |
| 146 | + |
| 147 | +Thus, the mempool selects transactions using an efficient and value-driven algorithm that ensures the most valuable transactions (in terms of PPU) are prioritized while maintaining correct nonce sequencing per sender. The selection process is as follows: |
| 148 | + |
| 149 | +**Setup phase:** |
| 150 | + |
| 151 | + - **Snapshot of senders:** |
| 152 | + - Before starting the selection loop, obtain a snapshot of all current senders in the mempool in an arbitrary order. |
| 153 | + |
| 154 | + - **Organize transactions into bunches:** |
| 155 | + - For each sender, collect all their pending transactions and organize them into a "bunch." |
| 156 | + - Each bunch is: |
| 157 | + - **Nonce-gap-free:** There are no missing nonces between transactions. |
| 158 | + - **Duplicates-free:** No duplicate transactions are included. |
| 159 | + - **Sorted by nonce:** Transactions are ordered in ascending order based on their nonce values. |
| 160 | + |
| 161 | + - **Prepare the heap:** |
| 162 | + - Extract the first transaction (lowest nonce) from each sender's bunch. |
| 163 | + - Place these transactions onto a max heap, which is ordered based on the transaction's PPU. |
| 164 | + |
| 165 | +**Selection loop:** |
| 166 | + |
| 167 | + - **Iterative selection:** |
| 168 | + - Continue the loop until either the total gas of selected transactions meets or exceeds `gasRequested`, or the number of selected transactions reaches `maxNum`. |
| 169 | + - In each iteration: |
| 170 | + - **Select the most valuable transaction:** |
| 171 | + - Pop the transaction with the highest PPU from the heap. |
| 172 | + - Append this transaction to the list of `selectedTransactions`. |
| 173 | + - **Update the sender's bunch:** |
| 174 | + - If the sender of the selected transaction has more transactions in their bunch: |
| 175 | + - Take the next transaction (next higher nonce) from the bunch. |
| 176 | + - Push this transaction onto the heap to compete in subsequent iterations. |
| 177 | + - This process ensures that at each step, the most valuable transaction across all senders is selected while maintaining proper nonce order for each sender. |
| 178 | + |
| 179 | + - **Early termination:** |
| 180 | + - The selection loop can terminate early if either of the following conditions is satisfied before all transactions are processed: |
| 181 | + - The accumulated gas of selected transactions meets or exceeds `gasRequested`. |
| 182 | + - The number of selected transactions reaches `maxNum`. |
| 183 | + |
| 184 | + |
| 185 | +### Paragraph 5 |
| 186 | + |
| 187 | +On the node's side, the selected transactions are shuffled using a deterministic algorithm. This shuffling ensures that the transaction order remains unpredictable to the proposer, effectively preventing _front-running attacks_. Therefore, being selected first by the mempool does not guarantee that a transaction will be included first in the block. Additionally, selection by the mempool does not ensure inclusion in the very next block, as the proposer has the final authority on which transactions to include, based on **the remaining space available** in the block. |
0 commit comments