{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Tx pool in EIP 1559\n", "\n", "---\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import os, sys\n", "sys.path.insert(1, os.path.realpath(os.path.pardir))\n", "# You may remove the two lines above if you have installed abm1559 from pypi\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" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "class OptimisticUser(User1559):\n", " def expected_time(self, params):\n", " return 1" ] }, { "cell_type": "code", "execution_count": 4, "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, params):\n", " return 1\n", "\n", " def decide_parameters(self, params):\n", " if params[\"min_premium\"] is None:\n", " min_premium = 1 * (10 ** 9)\n", " else:\n", " min_premium = params[\"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": [ "## (Bonus) Cancel culture\n", "\n", "We now allow users to cancel their transactions. We discussed in the previous notebook's bonus section the concept of _ex post individual rationality_. Suppose I estimate that should my transaction be included within 5 blocks, my payoff will be positive. In this case, I decide to join the pool and send my transaction. Fierce competition during transitionary periods however might prevent me from being included when I thought I would be. Without the ability to cancel my transaction, I must stick around in the transaction pool and could net a negative payoff once I am included.\n", "\n", "We add here the possibility for users to cancel their transaction. Cancelling is not completely trivial in the current, first-price auction paradigm (see [Etherscan's helpful guide for an overview](https://info.etherscan.com/how-to-cancel-ethereum-pending-transactions/)). To cancel transaction A, the trick is to send another transaction B with the same nonce (your address's \"transaction counter\") with a slightly higher fee. Miners with access to both A and B in their pool will strictly prefer B and _cannot_ include both.\n", "\n", "With EIP 1559, we'll assume users send a new transaction with a slightly higher premium. We also assumed that miners keep around an extensible list of pending transactions, even those rendered invalid from a high basefee, in case the basefee decreases. So we'll assume that as soon as a user sends a replacement transaction to cancel its original transaction, the original transaction disappears from the mempool. The costs to users vary according to the outcome:\n", "\n", "- In case the original and replacement transactions have a low gas price such that neither are ever included in a block, the user cancels \"for free\".\n", "- On the other hand, when the replacement transaction is included, the user incurs a cost. It is rational to cancel whenever the payoff from cancelling the transaction is greater than the payoff from inclusion. Suppose the replacement transaction posts a premium equal to $p'$ (gas price = $g'$), while the original transaction's is $p$ (gas price = $g$). Cancelling yields a payoff of $-g'$, while being included yields $v - w . c - g$, where $v$ is the value, $c$ is the cost for waiting per block and $w$ is the time waited. It is rational to cancel whenever:\n", "\n", "$$ v - w . c - g < -g' $$\n", "\n", "Of course, $g$ and $g'$ both depend on the current basefee, which complicates the matter. To simplify here, we assume that as soon as the current value $v - w . c$ of the user becomes negative, the user cancels their transaction." ] }, { "cell_type": "code", "execution_count": 18, "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 range(len(demand_scenario)):\n", " \n", " # `params` are the \"environment\" of the simulation\n", " params = {\n", " \"basefee\": basefee,\n", " \"current_block\": t,\n", " \"min_premium\": min_premium,\n", " \"cancel_cost\": 2 * (10 ** 9), # in wei/gas\n", " }\n", " \n", " #########\n", " ## ADDED\n", " ##\n", " # We ask whether current users want to cancel their transactions waiting in the pool\n", " cancelled_txs = user_pool.cancel_transactions(txpool, params) \n", " ##\n", " # Cancel transactions in the pool, adds new empty transactions with higher fee\n", " txpool.cancel_txs(cancelled_txs, params[\"cancel_cost\"])\n", " ##\n", " #########\n", " \n", " # We return some demand which on expectation yields 2000 new users per round\n", " users = spawn_poisson_heterogeneous_demand(t, demand_scenario[t], shares_scenario[t])\n", " \n", " # Add new users to the pool\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, params)\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(params)\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", " 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) is StrategicUser]),\n", " \"nonstategic\": len([user for user in users if type(user) is OptimisticUser]),\n", " \"decided_txs\": len(decided_txs),\n", " \"included_txs\": len(selected_txs),\n", " \"cancelled_txs\": len(cancelled_txs),\n", " \"blk_min_premium\": block.min_premium() / (10 ** 9), # to Gwei\n", " \"pool_length\": txpool.pool_length,\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": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "blocks = 200\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": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " | block | \n", "basefee | \n", "users | \n", "strategic | \n", "nonstategic | \n", "decided_txs | \n", "included_txs | \n", "cancelled_txs | \n", "blk_min_premium | \n", "pool_length | \n", "
---|---|---|---|---|---|---|---|---|---|---|
0 | \n", "0 | \n", "1.000000 | \n", "2498 | \n", "1249 | \n", "1249 | \n", "2170 | \n", "952 | \n", "0 | \n", "1.1 | \n", "1218 | \n", "
1 | \n", "1 | \n", "1.124900 | \n", "2595 | \n", "1297 | \n", "1298 | \n", "2242 | \n", "952 | \n", "0 | \n", "1.2 | \n", "2508 | \n", "
2 | \n", "2 | \n", "1.265400 | \n", "2409 | \n", "1204 | \n", "1205 | \n", "2086 | \n", "952 | \n", "37 | \n", "1.3 | \n", "3605 | \n", "
3 | \n", "3 | \n", "1.423448 | \n", "2534 | \n", "1267 | \n", "1267 | \n", "2122 | \n", "952 | \n", "57 | \n", "1.4 | \n", "4718 | \n", "
4 | \n", "4 | \n", "1.601237 | \n", "2618 | \n", "1309 | \n", "1309 | \n", "2190 | \n", "952 | \n", "84 | \n", "1.5 | \n", "5872 | \n", "
... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "
195 | \n", "195 | \n", "14.561289 | \n", "2520 | \n", "1260 | \n", "1260 | \n", "536 | \n", "536 | \n", "2 | \n", "1.0 | \n", "456 | \n", "
196 | \n", "196 | \n", "14.789901 | \n", "2461 | \n", "1230 | \n", "1231 | \n", "429 | \n", "429 | \n", "0 | \n", "1.0 | \n", "456 | \n", "
197 | \n", "197 | \n", "14.606691 | \n", "2483 | \n", "1241 | \n", "1242 | \n", "490 | \n", "490 | \n", "3 | \n", "1.0 | \n", "453 | \n", "
198 | \n", "198 | \n", "14.659640 | \n", "2468 | \n", "1234 | \n", "1234 | \n", "458 | \n", "458 | \n", "3 | \n", "1.0 | \n", "450 | \n", "
199 | \n", "199 | \n", "14.589641 | \n", "2570 | \n", "1285 | \n", "1285 | \n", "499 | \n", "499 | \n", "4 | \n", "1.0 | \n", "446 | \n", "
200 rows × 10 columns
\n", "