{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## TL;DR\n", "\n", "- EIP 1559 is a proposed improvement for the transaction fee market. It sets a variable \"base\" gasprice to be paid by the user and burned by the protocol, in addition to a \"tip\" paid by the user to the block producer.\n", "- When there are more users willing to pay the base price than there is space in the block, users are incentivised to compete against each other with the \"tip\".\n", "- Yet, once the basefee prices out all but a target size set of users, this strategic incentive disappears, and users can resume bidding the basefee plus a nominal tip.\n", "\n", "---\n", "\n", "In our [previous notebook](https://ethereum.github.com/abm1559/stationary1559.html), we simulated users interacting with the EIP 1559 transaction fee market mechanism for inclusion. Our users were strategic to some extent: they observed the prevailing basefee, evaluated their values and costs and decided whether to send a transaction or not. We saw that once the system reaches stationarity, users who aren't deterred by the basefee can expect next-block inclusion.\n", "\n", "We made one big assumption there, which is that users respect the commonly stated heuristic of setting their premiums (in general, what the miner receives from the transaction) at a fixed value compensating the miner for the extra work of including one extra transaction. In this notebook, we relax this assumption and look at what happens when users bid strategically, trying to outcompete each other in transitionary periods, before the basefee settles.\n", "\n", "Firstly, let's load up a few classes from our [`abm1559`](https://github.com/ethereum/abm1559) library." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import os, sys\n", "sys.path.insert(1, os.path.realpath(os.path.pardir))\n", "\n", "%config InlineBackend.figure_format = 'svg'\n", "\n", "from abm1559.utils import constants\n", "\n", "from abm1559.txpool import TxPool\n", "\n", "from abm1559.users import User1559\n", "\n", "from abm1559.userpool import UserPool\n", "\n", "from abm1559.chain import (\n", " Chain,\n", " Block1559,\n", ")\n", "\n", "from abm1559.simulator import (\n", " spawn_poisson_heterogeneous_demand,\n", " update_basefee,\n", ")\n", "\n", "import pandas as pd\n", "import numpy as np\n", "from tqdm import tqdm" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Introducing strategic users\n", "\n", "Our previous simulation only featured one type of users, `User1559`. Users received some random value (in Gwei, per gas unit) for transaction inclusion and costs (in Gwei, per gas and time unit) for waiting.\n", "\n", "For instance, Alice has 15 Gwei/gas value for inclusion, and cost 0.5 Gwei/gas/block. If Alice waits for 5 blocks to be included, her payoff for each gas unit she obtains is 15 - 5 * 0.5 = 12.5. To this, we subtract the fee Alice must pay for inclusion, or _gas price_. Assuming the basefee currently sits at 10 Gwei/gas and Alice set her premium at 1 Gwei/gas, her total gas price is 11 Gwei/gas. Her final payoff is then 12.5 - 11 = 1.5 Gwei/gas. In other words:\n", "\n", "$$ \\texttt{payoff} = \\texttt{value} - \\texttt{cost from waiting} - \\texttt{transaction fee} $$\n", "\n", "Users estimate their final payoff by estimating how long they will wait for inclusion. Our default `User1559` estimates a fixed waiting time of 5 blocks. Let's go ahead and change that to zero, as we have done with the `OptimisticUser` in the previous notebook. The `OptimisticUser` expects next block inclusion." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class OptimisticUser(User1559):\n", " def expected_time(self, env):\n", " return 0" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`OptimisticUser`s expect a small waiting time, but still set their premium to a fixed value. Can a strategic user do better? Let's find out!\n", "\n", "We'll subclass our `User1559` again to define the strategic user." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "class StrategicUser(User1559):\n", " \"\"\"\n", " A strategic affine user sending 1559 transactions.\n", " \n", " - Expects to be included in the next block\n", " - Prefers not to participate if its expected payoff is negative\n", " - Strategic gas_premium\n", " \"\"\"\n", " \n", " epsilon = 0.1 # how much the user overbids by\n", "\n", " def expected_time(self, env):\n", " return 0\n", "\n", " def decide_parameters(self, env):\n", " if env[\"min_premium\"] is None:\n", " min_premium = 1 * (10 ** 9)\n", " else:\n", " min_premium = env[\"min_premium\"]\n", "\n", " gas_premium = min_premium + self.epsilon * (10 ** 9)\n", " max_fee = self.value\n", "\n", " return {\n", " \"max_fee\": max_fee, # in wei\n", " \"gas_premium\": gas_premium, # in wei\n", " \"start_block\": self.wakeup_block,\n", " }\n", "\n", " def export(self):\n", " return {\n", " **super().export(),\n", " \"user_type\": \"strategic_user_1559\",\n", " }\n", "\n", " def __str__(self):\n", " return f\"1559 strategic affine user with value {self.value} and cost {self.cost_per_unit}\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As our `OptimisticUser` does, `StrategicUser` expects next-block inclusion. We also change `decide_parameters`: the strategic user sets its premium to _the minimum premium seen in the previous block_, plus 0.1 Gwei. Strategic users are trying to outbid the cheapest user (from the miner's point of view) who was included in the previous block.\n", "\n", "Users attach premiums to their transactions as well as fee caps. The fee cap is an instruction to not charge a gas price greater than the cap, whatever happens with the basefee, and protects users from accidental overbidding. Miners receive either the whole premium or the slack between current basefee and feecap, whichever is lower. We call this the _tip_.\n", "\n", "$$ \\texttt{tip} = \\min(\\texttt{fee cap} - \\texttt{basefee}, \\texttt{gas premium}) $$\n", "\n", "We set the fee cap of strategic users like we set it for non-strategic ones: since users receive at most their value from getting included, they set their cap to their value.\n", "\n", "### Mixed simulations\n", "\n", "We'll now put `OptimisticUser`s against `StrategicUser`s. We modify the `simulate` function we used previously to specify at each simulation step how much of each type we expect to spawn. These are the simulation steps:\n", "\n", "1. We sample from a Poisson distribution to obtain the number of new users spawning between two blocks.\n", "2. We spawn optimistic and strategic users according to the shares specified by `shares_scenario`.\n", "3. Each user decides whether to transact or not (`decide_transactions`).\n", "4. Transactions are added to the transaction pool.\n", "5. The miner decides on a set of transactions to include, ranking transactions according to their tips, with higher tips at the top of the list.\n", "6. A block is created and added to the chain.\n", "7. Basefee is updated.\n", "8. Repeat." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "def simulate(demand_scenario, shares_scenario):\n", " # Instantiate a couple of things\n", " txpool = TxPool()\n", " basefee = constants[\"INITIAL_BASEFEE\"]\n", " chain = Chain()\n", " metrics = []\n", " user_pool = UserPool()\n", " min_premium = 1 * (10 ** 9)\n", "\n", " for t in tqdm(range(len(demand_scenario))):\n", " \n", " # `env` is the \"environment\" of the simulation\n", " env = {\n", " \"basefee\": basefee,\n", " \"current_block\": t,\n", " \"min_premium\": min_premium,\n", " }\n", " \n", " # We return some demand which on expectation yields demand_scenario[t] new users per round\n", " users = spawn_poisson_heterogeneous_demand(t, demand_scenario[t], shares_scenario[t])\n", " \n", " # Add users to the pool and check who wants to transact\n", " # We query each new user with the current basefee value\n", " # Users either return a transaction or None if they prefer to balk\n", " decided_txs = user_pool.decide_transactions(users, env)\n", "\n", " # New transactions are added to the transaction pool\n", " txpool.add_txs(decided_txs)\n", "\n", " # The best valid transactions are taken out of the pool for inclusion\n", " selected_txs = txpool.select_transactions(env)\n", " txpool.remove_txs([tx.tx_hash for tx in selected_txs])\n", "\n", " # We create a block with these transactions\n", " block = Block1559(txs = selected_txs, parent_hash = chain.current_head, height = t, basefee = basefee)\n", "\n", " # Record the min premium in the block\n", " min_premium = block.min_premium()\n", " \n", " # The block is added to the chain\n", " chain.add_block(block)\n", " \n", " # A couple of metrics we will use to monitor the simulation\n", " pool_strat_users = len(\n", " [tx for tx in txpool.txs.values() if type(user_pool.users[tx.sender]) == StrategicUser])\n", " pool_nonstrat_users = len(\n", " [tx for tx in txpool.txs.values() if type(user_pool.users[tx.sender]) == OptimisticUser])\n", "\n", " row_metrics = {\n", " \"block\": t,\n", " \"basefee\": basefee / (10 ** 9),\n", " \"users\": len(users),\n", " \"strategic\": len([user for user in users if type(user) == StrategicUser]),\n", " \"nonstategic\": len([user for user in users if type(user) == OptimisticUser]),\n", " \"decided_txs\": len(decided_txs),\n", " \"included_txs\": len(selected_txs),\n", " \"blk_min_premium\": block.min_premium() / (10 ** 9), # to Gwei\n", " \"blk_avg_gas_price\": block.average_gas_price(),\n", " \"blk_avg_tip\": block.average_tip(),\n", " \"pool_length\": txpool.pool_length(),\n", " \"pool_strat_users\": pool_strat_users,\n", " \"pool_nonstrat_users\": pool_nonstrat_users,\n", " }\n", " metrics.append(row_metrics)\n", "\n", " # Finally, basefee is updated and a new round starts\n", " basefee = update_basefee(block, basefee)\n", "\n", " return (pd.DataFrame(metrics), user_pool, chain)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We'll start with a simulation for 200 blocks. We set equal shares of strategic and non-strategic users, with on average a total of 2500 users spawning each round. Remember that our blocks can accommodate at most 952 users, but target inclusion of about half of this number, i.e., 475 of them. There is plenty of space then for strategic users (on average 1250 of them each round) to grab all the available space." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "100%|██████████| 100/100 [00:13<00:00, 7.34it/s]\n" ] } ], "source": [ "blocks = 100\n", "demand_scenario = [2500 for i in range(blocks)]\n", "\n", "strategic_share = 0.5\n", "shares_scenario = [{\n", " StrategicUser: strategic_share,\n", " OptimisticUser: 1 - strategic_share,\n", "} for i in range(blocks)]\n", "\n", "(df, user_pool, chain) = simulate(demand_scenario, shares_scenario)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "What does `df` reveal?" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
blockbasefeeusersstrategicnonstategicdecided_txsincluded_txsblk_min_premiumblk_avg_gas_priceblk_avg_tippool_lengthpool_strat_userspool_nonstrat_users
001.000000257112851286231311901.02.0971431.097143112301123
111.124900252112601261225111901.02.2193541.094454218402184
221.265400246512321233218611901.02.3568291.091429318003180
331.423448247512371238216311901.02.5142051.090756415304153
441.601237251312561257221011901.02.6936741.092437517305173
..........................................
959514.3554732551127512765885881.015.4041131.048639893008930
969614.3336532531126512665655651.015.3826801.049027893008930
979714.2426342644132213226096091.015.2968221.054187893008930
989814.2837962530126512655895891.015.3318431.048048893008930
999914.2650842419120912105535531.015.3169831.051899893008930
\n", "

100 rows × 13 columns

\n", "
" ], "text/plain": [ " block basefee users strategic nonstategic decided_txs \\\n", "0 0 1.000000 2571 1285 1286 2313 \n", "1 1 1.124900 2521 1260 1261 2251 \n", "2 2 1.265400 2465 1232 1233 2186 \n", "3 3 1.423448 2475 1237 1238 2163 \n", "4 4 1.601237 2513 1256 1257 2210 \n", ".. ... ... ... ... ... ... \n", "95 95 14.355473 2551 1275 1276 588 \n", "96 96 14.333653 2531 1265 1266 565 \n", "97 97 14.242634 2644 1322 1322 609 \n", "98 98 14.283796 2530 1265 1265 589 \n", "99 99 14.265084 2419 1209 1210 553 \n", "\n", " included_txs blk_min_premium blk_avg_gas_price blk_avg_tip \\\n", "0 1190 1.0 2.097143 1.097143 \n", "1 1190 1.0 2.219354 1.094454 \n", "2 1190 1.0 2.356829 1.091429 \n", "3 1190 1.0 2.514205 1.090756 \n", "4 1190 1.0 2.693674 1.092437 \n", ".. ... ... ... ... \n", "95 588 1.0 15.404113 1.048639 \n", "96 565 1.0 15.382680 1.049027 \n", "97 609 1.0 15.296822 1.054187 \n", "98 589 1.0 15.331843 1.048048 \n", "99 553 1.0 15.316983 1.051899 \n", "\n", " pool_length pool_strat_users pool_nonstrat_users \n", "0 1123 0 1123 \n", "1 2184 0 2184 \n", "2 3180 0 3180 \n", "3 4153 0 4153 \n", "4 5173 0 5173 \n", ".. ... ... ... \n", "95 8930 0 8930 \n", "96 8930 0 8930 \n", "97 8930 0 8930 \n", "98 8930 0 8930 \n", "99 8930 0 8930 \n", "\n", "[100 rows x 13 columns]" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see on average 2500 `users`, with half of them `strategic` and the other half `nonstrategic`. Many decide to transact in the first few rounds, more than are eventually included (`decided_txs` > `included_txs`). The pool length, i.e., how many decided transactions were not included yet, increases steadily, with most users in the pool non-strategic users! We have the first clue that being strategic is not a bad idea, so let's dig a little more in results.\n", "\n", "### Strategic users enter a bidding war\n", "\n", "Let's look at the basefee and the minimum premium in a block." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", " \n", " \n", " \n", " \n", " 2021-06-13T18:30:02.246021\n", " image/svg+xml\n", " \n", " \n", " Matplotlib v3.4.2, https://matplotlib.org/\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ], "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "df.plot(\"block\", [\"basefee\", \"blk_min_premium\"])" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
blockbasefeeusersstrategicnonstategicdecided_txsincluded_txsblk_min_premiumblk_avg_gas_priceblk_avg_tippool_lengthpool_strat_userspool_nonstrat_users
242416.85520425401270127027011901.017.8672201.01201711787011787
252518.96041825281264126441511.019.4882510.52783211640011640
262617.1916012558127912792067481.017.8655050.67390411098011098
272717.7431082492124612461481481.018.7924321.04932411098011098
\n", "
" ], "text/plain": [ " block basefee users strategic nonstategic decided_txs \\\n", "24 24 16.855204 2540 1270 1270 270 \n", "25 25 18.960418 2528 1264 1264 4 \n", "26 26 17.191601 2558 1279 1279 206 \n", "27 27 17.743108 2492 1246 1246 148 \n", "\n", " included_txs blk_min_premium blk_avg_gas_price blk_avg_tip \\\n", "24 1190 1.0 17.867220 1.012017 \n", "25 151 1.0 19.488251 0.527832 \n", "26 748 1.0 17.865505 0.673904 \n", "27 148 1.0 18.792432 1.049324 \n", "\n", " pool_length pool_strat_users pool_nonstrat_users \n", "24 11787 0 11787 \n", "25 11640 0 11640 \n", "26 11098 0 11098 \n", "27 11098 0 11098 " ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df[(df.block >= 24) & (df.block <= 27)]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Basefee increases until reaching its apex at block 26. This is the same behaviour we observed in the previous notebook: too many users want in so the basefee quickly reaches a high value, where no one is willing to pay this much. After the apex, the basefee climbs down, accommodating some of the most high-valued new users as well as equally high-valued users in the pool who were priced out.\n", "\n", "We also plot (as seen in orange in the previous graph) the minimum premium observed in the block. The dynamics are curious: to understand them, remember that strategic users systematically bid just above the minimum premium observed in the previous block. During the transitionary period between blocks 0 and 26, too many users want in so the basefee increases. At the same time, the minimum premium observed in a block increases too. We can imagine that enough strategic users successfully outbid non-strategic ones to fill the whole block. Since strategic users always bid above the smallest premium in the previous block, they bid a premium of 1.1 Gwei the first time, then 1.2, then 1.3 etc. At this point, we recognise the unstable dynamics of the current first-price auction paradigm, where users bid _against each other_ instead of bidding their true value.\n", "\n", "As the basefee increases, fewer and fewer new strategic users decide to transact, since **low-valued users are priced out**, leaving some space for non-strategic users to be included. This releases some pressure on the premiums, until block 26 where the basefee is so high that no one is included (and the minimum premium is zero)." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", " \n", " \n", " \n", " \n", " 2021-06-13T18:30:02.407807\n", " image/svg+xml\n", " \n", " \n", " Matplotlib v3.4.2, https://matplotlib.org/\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ], "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "df.plot(\"block\", [\"pool_length\", \"users\", \"pool_strat_users\", \"pool_nonstrat_users\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We observe in the chart above the transaction pool length, and indeed confirm that most users in the pool are non-strategic. **Strategic users get ahead by outbidding them**.\n", "\n", "### When basefee settles, strategic behaviour does not help\n", "\n", "We'll now look at the trace more closely. We export simulation data to pandas `DataFrame`s for ease of manipulation, using the `export` methods we defined in our classes." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "# Obtain the pool of users (all users spawned by the simulation)\n", "user_pool_df = user_pool.export().rename(columns={ \"pub_key\": \"sender\" })\n", "\n", "# Export the trace of the chain, all transactions included in blocks\n", "chain_df = chain.export()\n", "\n", "# Join the two to associate transactions with their senders\n", "user_txs_df = chain_df.join(user_pool_df.set_index(\"sender\"), on=\"sender\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For now we'll only look at the distribution of strategic vs. non-strategic users in the blocks." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "# Obtain per user type statistics\n", "txs_per_user_type = user_txs_df.groupby(\n", " [\"block_height\", \"user_type\"]\n", ").agg(\n", " { \"user_type\": len }\n", ").unstack(level=-1).reset_index()\n", "\n", "txs_per_user_type[\"user_type\"] = txs_per_user_type[\"user_type\"].fillna(0)\n", "\n", "txs_per_user_type.columns = [\"block_height\", \"strategic\", \"nonstrategic\"]\n", "\n", "txs_per_user_type[\"total\"] = txs_per_user_type.apply(\n", " lambda row: row.strategic + row.nonstrategic,\n", " axis = 1\n", ")\n", "txs_per_user_type[\"percent_strategic\"] = txs_per_user_type.apply(\n", " lambda row: row.strategic / row.total * 100,\n", " axis = 1\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We first check how many users in each block are strategic/non-strategic." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", " \n", " \n", " \n", " \n", " 2021-06-13T18:30:04.417602\n", " image/svg+xml\n", " \n", " \n", " Matplotlib v3.4.2, https://matplotlib.org/\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ], "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "txs_per_user_type.plot(\"block_height\", [\"strategic\", \"nonstrategic\", \"total\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "From this plot, a few observations stand out:\n", " \n", "1. For the first few blocks, only strategic users are included.\n", "2. This changes before the basefee reaches its apex (at block 26), as more and more non-strategic users are included. By block 20, more non-strategic users are included than strategic.\n", "3. There are weird sawtooths between blocks 25 and 50. I don't believe these have much interpretation, having more to do with discontinuous behaviour of the simulation. When the basefee drops by a small amount, some users in the pool are included, sometimes enough for the basefee to increase again, after which most users cannot get in. This keeps on going until all users left in the pool have a fee cap lower than the stationary basefee level.\n", "4. Once series stabilise, we observe that strategic and non-strategic users get in at equal ratio. Remember that we spawn equal numbers of strategic and non-strategic users each round, so we may conclude here that strategic users lost their overbidding advantage: at this point, the basefee is more determinant to decide who gets in or not.\n", "\n", "This is confirmed by the following plot, which shows the percentage of strategic users in each block." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", " \n", " \n", " \n", " \n", " 2021-06-13T18:30:04.573826\n", " image/svg+xml\n", " \n", " \n", " Matplotlib v3.4.2, https://matplotlib.org/\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ], "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "txs_per_user_type.plot(\"block_height\", \"percent_strategic\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Piecing parts together\n", "\n", "In the chart above, between blocks 0 and 26, before basefee reaches its apex, strategic users are included in much higher proportions than non-strategic users. Not surprising, since they post higher premiums. More surprising however is that this proportion is decreasing, until it is reversed: by block 20, more non-strategic users are included than strategic.\n", "\n", "This is trickier to explain but we can sketch the following narrative.\n", "\n", "1. Most users, both strategic and non-strategic, have valid transactions (for which the cap is above the basefee) to send. Strategic users get in first and take all the slots.\n", "2. Users start being discriminated by the basefee. Strategic users with valid transactions get in, some non-strategic users get in too.\n", "3. Basefee keeps increasing. _High-valued_ non-strategic users which were languishing in the pool start filling the block, while most new, _low-valued_ strategic users are priced out.\n", "4. Basefee hits highest level. No one gets in.\n", "5. Basefee starts decreasing. Valid leftovers in the pool (almost all of them non-strategic) are included with some of the new users.\n", "6. Basefee stabilises. Only new users are included, no one in the pool is valid anymore.\n", "\n", "It is expected to find this instability during transitionary periods, where the basefee needs to adapt to a changing demand, e.g., a spike in transactions or higher values for transacting. We find however that once the basefee settles, strategic users no longer have the edge over nonstrategic ones: the basefee is the true determinant for inclusion." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Value bidding\n", "\n", "In this section, we change the bidding behaviour once more. Previously, we looked at `OptimisticUser`s who set their gas premium to 1 Gwei, a small, fixed value meant to compensate miner work, or `StrategicUser`s who overbid when blocks are full. With EIP 1559 implemented, it is reasonable to believe most users would follow this simple bidding strategy: look at the gasprice level (the current basefee plus this 1 Gwei premium) and decide sending their transaction or not, sometimes adjusting the premium when congestion spikes.\n", "\n", "We'll relax this assumption however, and look at the case where users **set their premium according to their value for the transaction**: the higher their value, the greater the premium." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "class OptimisticUser(User1559):\n", " def expected_time(self, env):\n", " return 0\n", " \n", " def decide_parameters(self, env):\n", " # Users add a fraction of their value to their premium\n", " # Higher value users thus have higher premiums, all else equal\n", " gas_premium = 1 * (10 ** 9) + self.value // 1000\n", " max_fee = self.value\n", " \n", " return {\n", " \"max_fee\": max_fee,\n", " \"gas_premium\": gas_premium, # in wei\n", " \"start_block\": self.wakeup_block,\n", " }\n", " \n", "class StrategicUser(User1559):\n", " \"\"\"\n", " A strategic affine user sending 1559 transactions.\n", " \n", " - Expects to be included in the next block\n", " - Prefers not to participate if its expected payoff is negative\n", " - Strategic gas_premium\n", " \"\"\"\n", " \n", " epsilon = 0.1 # how much the user overbids by\n", "\n", " def expected_time(self, env):\n", " return 0\n", "\n", " def decide_parameters(self, env):\n", " if env[\"min_premium\"] is None:\n", " min_premium = 1 * (10 ** 9)\n", " else:\n", " min_premium = env[\"min_premium\"]\n", "\n", " gas_premium = min_premium + self.value // 1000 + self.epsilon * (10 ** 9)\n", " max_fee = self.value\n", "\n", " return {\n", " \"max_fee\": max_fee, # in wei\n", " \"gas_premium\": gas_premium, # in wei\n", " \"start_block\": self.wakeup_block,\n", " }\n", "\n", " def export(self):\n", " return {\n", " **super().export(),\n", " \"user_type\": \"strategic_user_1559\",\n", " }\n", "\n", " def __str__(self):\n", " return f\"1559 strategic affine user with value {self.value} and cost {self.cost_per_unit}\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Run the simulation again." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "100%|██████████| 100/100 [00:12<00:00, 7.79it/s]\n" ] } ], "source": [ "strategic_share = 0.5\n", "shares_scenario = [{\n", " StrategicUser: strategic_share,\n", " OptimisticUser: 1 - strategic_share,\n", "} for i in range(blocks)]\n", "\n", "(df, user_pool, chain) = simulate(demand_scenario, shares_scenario)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Basefee and minimum premiums have similar dynamics." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", " \n", " \n", " \n", " \n", " 2021-06-13T18:30:17.563225\n", " image/svg+xml\n", " \n", " \n", " Matplotlib v3.4.2, https://matplotlib.org/\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ], "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "df.plot(\"block\", [\"basefee\", \"blk_min_premium\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We obtain the average value of included strategic and non-strategic users." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", " \n", " \n", " \n", " \n", " 2021-06-13T18:30:19.298154\n", " image/svg+xml\n", " \n", " \n", " Matplotlib v3.4.2, https://matplotlib.org/\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ], "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# Obtain the pool of users (all users spawned by the simulation)\n", "user_pool_df = user_pool.export().rename(columns={ \"pub_key\": \"sender\" })\n", "\n", "# Export the trace of the chain, all transactions included in blocks\n", "chain_df = chain.export()\n", "\n", "# Join the two to associate transactions with their senders\n", "user_txs_df = chain_df.join(user_pool_df.set_index(\"sender\"), on=\"sender\")\n", "\n", "# Obtain per user type statistics\n", "txs_per_user_type = user_txs_df.groupby(\n", " [\"block_height\", \"user_type\"]\n", ").agg(\n", " { \"value\": np.mean }\n", ").unstack(level=-1).reset_index()\n", "\n", "txs_per_user_type[\"value\"] = txs_per_user_type[\"value\"].fillna(0)\n", "\n", "txs_per_user_type.columns = [\"block_height\", \"avg_strategic_value\", \"avg_nonstrategic_value\"]\n", "txs_per_user_type[\"blk_min_premium\"] = df[\"blk_min_premium\"]\n", "\n", "txs_per_user_type.plot(\"block_height\", [\"avg_strategic_value\", \"avg_nonstrategic_value\", \"blk_min_premium\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We recognise familiar dynamics: for the first 10 blocks or so, non-strategic users are not included, being outbid by strategic users. Once the basefee prices out enough transactions, there is room for strategic users to join. High-value users get in first, since their premium increases with their value. Yet once the basefee stabilises, both groups of included users have equal average value.\n", "\n", "It is useful to obtain a counterfactual, to check what the results would be with all users non-strategic. We set the `strategic_share` to zero and run the simulation again." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "100%|██████████| 100/100 [00:13<00:00, 7.61it/s]\n" ] } ], "source": [ "strategic_share = 0\n", "shares_scenario = [{\n", " StrategicUser: strategic_share,\n", " OptimisticUser: 1 - strategic_share,\n", "} for i in range(blocks)]\n", "\n", "(df2, user_pool2, chain2) = simulate(demand_scenario, shares_scenario)" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "# Obtain the pool of users (all users spawned by the simulation)\n", "user_pool_df2 = user_pool2.export().rename(columns={ \"pub_key\": \"sender\" })\n", "\n", "# Export the trace of the chain, all transactions included in blocks\n", "chain_df2 = chain2.export()\n", "\n", "# Join the two to associate transactions with their senders\n", "user_txs_df2 = chain_df2.join(user_pool_df2.set_index(\"sender\"), on=\"sender\")\n", "\n", "# Obtain per user type statistics\n", "txs_per_user_type2 = user_txs_df2.groupby(\n", " [\"block_height\", \"user_type\"]\n", ").agg(\n", " { \"value\": [\"mean\"] }\n", ").unstack(level=-1).fillna(0).reset_index()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We look at the average value of included users in our original scenario and the counterfactual." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(10.0, 21.0)" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", " \n", " \n", " \n", " \n", " 2021-06-13T18:30:33.986717\n", " image/svg+xml\n", " \n", " \n", " Matplotlib v3.4.2, https://matplotlib.org/\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ], "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "compare = pd.DataFrame({\n", " \"block_height\": txs_per_user_type.iloc[:,0],\n", " \"strat\": txs_per_user_type.iloc[:,1],\n", " \"nonstrat\": txs_per_user_type.iloc[:,2],\n", " \"nonstrat_counterfact\": txs_per_user_type2.iloc[:,1] })\n", "\n", "compare.plot(\"block_height\", [\"strat\", \"nonstrat\", \"nonstrat_counterfact\"]).set_ylim([10, 21])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the counterfactual scenario, where all users are non-strategic, the average value of included users in the first few blocks is around 16 Gwei per gas. Meanwhile, the average value of included users in the original scenario starts from 12 Gwei per gas!\n", "\n", "We see here the impact of various bidding behaviours on **efficiency**. Auctions are typically deemed efficient when high-value users get what what they want, e.g., when the item being auctioned goes to who wants it the most. When users bid as they do in a first-price auction, the fee market is less efficient than when users are non-strategic. Fortunately, **this inefficiency doesn't last**: once the basefee reaches its stationary level, in both the original scenario as well as the counterfactual, similar-valued users are included (namely, the top 475 of them).\n", "\n", "In future notebooks, we'll investigate the efficiency properties of different fee market mechanisms." ] }, { "cell_type": "raw", "metadata": {}, "source": [ "Strategic users in EIP 1559" ] }, { "cell_type": "raw", "metadata": {}, "source": [] }, { "cell_type": "raw", "metadata": {}, "source": [ "// References + footnotes\n", "\n", "// Authors\n", "let authorData = [\"barnabe\"];" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "Many thanks to Fred Lacs for comments and code." ] } ], "metadata": { "desc": "When keeping the tip low, goes wrong", "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.5" }, "path": "notebooks/strategicUser.html", "repo": "abm1559", "title": "Strategic users in EIP 1559" }, "nbformat": 4, "nbformat_minor": 4 }