{ "cells": [ { "cell_type": "code", "execution_count": 1, "id": "divided-south", "metadata": {}, "outputs": [], "source": [ "%config InlineBackend.figure_format = 'svg'\n", "\n", "import os, sys\n", "sys.path.insert(1, os.path.realpath(os.path.pardir))\n", "\n", "from typing import Sequence, Dict, Tuple\n", "\n", "from abm1559.utils import (\n", " constants,\n", ")\n", "\n", "gamma = 76000 # obtained from stats analysis of sample txs\n", "constants[\"SIMPLE_TRANSACTION_GAS\"] = gamma\n", "\n", "from abm1559.txpool import TxPool\n", "\n", "from abm1559.users import (\n", " User1559,\n", " User\n", ")\n", "\n", "from abm1559.config import rng\n", "\n", "from abm1559.txs import Tx1559\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", " generate_gbm,\n", " apply_block_time_variance,\n", " generate_poisson_process,\n", " generate_abm,\n", " generate_jump_process,\n", ")\n", "\n", "import matplotlib.pyplot as plt\n", "import pandas as pd\n", "pd.set_option('display.max_rows', 1000)\n", "import numpy as np\n", "import time\n", "from tqdm.notebook import tqdm\n", "from itertools import product" ] }, { "cell_type": "code", "execution_count": 2, "id": "cooked-alias", "metadata": {}, "outputs": [], "source": [ "MAX_TX_POOL = 4096\n", "MIN_PREMIUM = 1e9\n", "\n", "class TxPool1559(TxPool):\n", " \n", " def __init__(self, max_txs=MAX_TX_POOL, min_premium=MIN_PREMIUM, **kwargs):\n", " super().__init__(**kwargs)\n", " self.max_txs = max_txs\n", " self.min_premium = MIN_PREMIUM\n", " \n", " def add_txs(self, txs: Sequence[Tx1559], env: dict) -> Sequence[Tx1559]:\n", " for tx in txs:\n", " self.txs[tx.tx_hash] = tx\n", " \n", " if self.pool_length() > self.max_txs:\n", " sorted_txs = sorted(self.txs.values(), key = lambda tx: -tx.tip(env))\n", " self.empty_pool()\n", " self.add_txs(sorted_txs[0:self.max_txs], env)\n", " return sorted_txs[self.max_txs:]\n", " \n", " return []\n", " \n", " def select_transactions(self, env, user_pool=None, rng=rng) -> Sequence[Tx1559]:\n", " # Miner side\n", " max_tx_in_block = int(constants[\"MAX_GAS_EIP1559\"] / constants[\"SIMPLE_TRANSACTION_GAS\"])\n", "\n", " valid_txs = [tx for tx in self.txs.values() if tx.is_valid(env) and tx.tip(env) >= self.min_premium]\n", " rng.shuffle(valid_txs)\n", "\n", " sorted_valid_demand = sorted(\n", " valid_txs,\n", " key = lambda tx: -tx.tip(env)\n", " )\n", " selected_txs = sorted_valid_demand[0:max_tx_in_block]\n", "\n", " return selected_txs\n", " \n", "class TxPoolTrendPicker(TxPool1559):\n", " \n", " # window = 1 <=> Fixed band policy\n", " \n", " def __init__(self, band_width=0.1, window=1, **kwargs):\n", " super().__init__(**kwargs)\n", " self.band_width = band_width\n", " self.window = window\n", " \n", " def should_evict(self, tx: Tx1559, basefee: int, delta: float) -> bool:\n", " # band_width = 1 <=> never evict\n", " return (1+delta) * (1-self.band_width) * basefee > tx.max_fee\n", " \n", " def apply_eviction_policy(self, txs: Sequence[Tx1559], env: dict) -> Tuple[Sequence[Tx1559], Sequence[Tx1559]]:\n", " current_basefee = env[\"basefees\"][-1]\n", " moving_average = sum(env[\"basefees\"]) / len(env[\"basefees\"]) if len(env[\"basefees\"]) < self.window else sum([env[\"basefees\"][-i] for i in range(1, self.window+1)]) // self.window\n", " delta = (current_basefee - moving_average) / current_basefee\n", " evicted_txs = [tx for tx in txs if self.should_evict(tx, current_basefee, delta)]\n", " accepted_txs = [tx for tx in txs if not self.should_evict(tx, current_basefee, delta)]\n", " return (evicted_txs, accepted_txs)\n", " \n", " def purge_pool_after_basefee_update(self, env: dict) -> Sequence[Tx1559]:\n", " evicted_txs, _ = self.apply_eviction_policy(self.txs.values(), env)\n", " self.remove_txs([tx.tx_hash for tx in evicted_txs])\n", " return evicted_txs" ] }, { "cell_type": "code", "execution_count": 3, "id": "pointed-paint", "metadata": {}, "outputs": [], "source": [ "class StrategicUser(User1559):\n", " \n", " def __init__(self, wakeup_block, **kwargs):\n", " super().__init__(wakeup_block, cost_per_unit = 0, **kwargs)\n", "# self.value = (1 + self.rng.pareto(2) * 20) * 1e9\n", " self.value = self.rng.uniform(10, 100) * 1e9\n", "\n", " def decide_parameters(self, env):\n", " # If previous block was close to full, consider being strategic\n", " if env[\"wallet_mode\"] == \"posted\":\n", " gas_premium = MIN_PREMIUM\n", " max_fee = min(max(env[\"basefee\"] * 3, MIN_PREMIUM * 3), self.value)\n", " else:\n", " max_fee = min(max(env[\"basefee\"] * 3, MIN_PREMIUM * 3), self.value)\n", " gas_premium = min(max_fee, env[\"previous_avg_tip\"] + MIN_PREMIUM)\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 create_transaction(self, env):\n", " \n", " tx_params = self.decide_parameters(env)\n", "\n", " tx = Tx1559(\n", " sender = self.pub_key,\n", " tx_params = tx_params,\n", " )\n", "\n", " return tx\n", "\n", " def export(self):\n", " return {\n", " **super().export(),\n", " \"user_type\": \"strategic_user\",\n", " }" ] }, { "cell_type": "markdown", "id": "automotive-empire", "metadata": {}, "source": [ "---\n", "\n", "## Demand generation" ] }, { "cell_type": "code", "execution_count": 7, "id": "exclusive-metallic", "metadata": {}, "outputs": [], "source": [ "def generate_gbm_demand(S0: float, duration: int, blocks: int, volatility: float, mean_ia_time: float, rng: np.random.Generator) -> Sequence[int]:\n", " mu = 0.5 * volatility**2\n", " gbm = list(generate_gbm(S0, duration, paths=1, mu=mu, sigma=volatility, rng=rng).flatten())\n", " return apply_block_time_variance(gbm, blocks, mean_ia_time=mean_ia_time, rng=rng)" ] }, { "cell_type": "code", "execution_count": 8, "id": "cooperative-initial", "metadata": {}, "outputs": [], "source": [ "def generate_decaying_abm_demand(S0: float, duration: int, blocks: int, volatility: float, mean_ia_time: float, rng: np.random.Generator) -> Sequence[int]:\n", " pp = poisson_process(0.001, duration, rng)\n", " jp = jump_process(pp, duration, S0, rng, discount = 0.01)\n", " abm = list(generate_abm(S0, duration, paths=1, mu=0, sigma=volatility, rng=rng).flatten())\n", " return apply_block_time_variance(abm + jp, blocks, mean_ia_time=mean_ia_time, rng=rng)" ] }, { "cell_type": "markdown", "id": "central-secondary", "metadata": {}, "source": [ "---" ] }, { "cell_type": "code", "execution_count": 9, "id": "adjustable-magnet", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "", "version_major": 2, "version_minor": 0 }, "text/plain": [ "session loop: 0%| | 0/720 [00:00 5 * MAX_TX_POOL:\n", " break\n", " \n", " # Sets current block\n", " env[\"current_block\"] = t\n", " \n", " # Reset the random number generator with new seed to generate users with same values across runs\n", " rng = np.random.default_rng((2 ** t) * (3 ** demand_id))\n", " \n", " ### SIMULATION ###\n", " \n", " # The transaction pool applies its eviction policy after basefee update\n", " basefee_update_evicted_txs = txpool.purge_pool_after_basefee_update(env)\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], rng=rng)\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, env)\n", " \n", " # Divide incoming transactions between accepted and rejected ones\n", " incoming_evicted_txs, accepted_txs = txpool.apply_eviction_policy(decided_txs, env)\n", "\n", " # New transactions are added to the transaction pool\n", " # `evicted_txs` holds the transactions removed from the pool for lack of space\n", " pool_limit_evicted_txs = txpool.add_txs(accepted_txs, env)\n", "\n", " # The best valid transactions are taken out of the pool for inclusion\n", " selected_txs = txpool.select_transactions(env, rng = rng)\n", " \n", " txpool.remove_txs([tx.tx_hash for tx in selected_txs])\n", "\n", " # We create a block with these transactions\n", " block = Block1559(\n", " txs = selected_txs, parent_hash = chain.current_head,\n", " height = t, basefee = env[\"basefee\"]\n", " )\n", " \n", " # Record the gas used and avg tip in the block\n", " env[\"previous_gas_used\"] = int(block.gas_used() / constants[\"MAX_GAS_EIP1559\"] * 100)\n", " env[\"previous_avg_tip\"] = int(block.average_tip() * 1e9)\n", " env[\"wallet_mode\"] = \"posted\" if env[\"previous_gas_used\"] <= 90 else \"expert\"\n", " \n", " # The block is added to the chain\n", " chain.add_block(block)\n", " \n", " ### METRICS ###\n", " user_efficiency = sum([user_pool.get_user(tx.sender).value for tx in selected_txs])\n", " \n", " row_metrics = {\n", " \"block\": t,\n", " \"basefee_update_evictions\": len(basefee_update_evicted_txs),\n", " \"users\": len(users),\n", " \"decided_txs\": len(decided_txs),\n", " \"incoming_evictions\": len(incoming_evicted_txs),\n", " \"pool_limit_evictions\": len(pool_limit_evicted_txs),\n", " \"included_txs\": len(selected_txs),\n", " \"basefee\": env[\"basefee\"] / 1e9, # to Gwei\n", " \"gas_used\": env[\"previous_gas_used\"],\n", " \"avg_tip\": env[\"previous_avg_tip\"] / 1e9,\n", " \"pool_length\": txpool.pool_length(),\n", " \"user_efficiency\": user_efficiency,\n", " }\n", " \n", " if not extra_metrics is None:\n", " row_metrics = {\n", " **row_metrics,\n", " **extra_metrics(env, users, user_pool, txpool),\n", " }\n", " \n", " metrics.append(row_metrics)\n", " \n", " # Finally, basefee is updated and a new round starts\n", " env[\"basefee\"] = update_basefee(block, env[\"basefee\"])\n", " env[\"basefees\"] = env[\"basefees\"][-(max_basefee_window-1):] + [env[\"basefee\"]]\n", "\n", " return (pd.DataFrame(metrics), user_pool, chain)\n", "\n", "volatilities = [0.1, 0.5, 1.0]\n", "# volatilities = [1.0]\n", "demand_paths = range(20)\n", "band_widths = [0.0, 1.0/3, 2.0/3, 1.0]\n", "# band_widths = [0.0]\n", "blocks = 600\n", "metrics = {}\n", "dfs = []\n", "run = 0\n", "gamma = 76000\n", "mean_ia_time = 13\n", "max_k = int(2 * blocks * mean_ia_time)\n", "max_txs_in_block = int(constants[\"MAX_GAS_EIP1559\"] / gamma)\n", "S0s = [int(k * max_txs_in_block / mean_ia_time) for k in [1, 1.5, 2]]\n", "\n", "for params in tqdm(list(product(\n", " demand_paths, volatilities, band_widths, S0s\n", ")), desc=\"session loop\", leave=False):\n", " (demand_path, volatility, band_width, S0) = params\n", " \n", " # Generate demand\n", " rng = np.random.default_rng(demand_path)\n", " demand_scenario = np.maximum(generate_decaying_abm_demand(S0, max_k, blocks, volatility, mean_ia_time, rng), 0)\n", " \n", " # Shares of new users per time step\n", " shares_scenario = [{\n", " StrategicUser: 1.00,\n", " } for t in range(blocks)]\n", "\n", " # Transaction pool\n", " txpool = TxPoolTrendPicker(band_width = band_width)\n", " \n", " # Simulate\n", " (df, user_pool, chain) = simulate(demand_scenario, demand_path, shares_scenario, txpool, silent=True)\n", " df[\"run\"] = run\n", " df[\"volatility\"] = volatility\n", " df[\"band_width\"] = band_width\n", " df[\"demand_path\"] = demand_path\n", " df[\"S0\"] = S0\n", " run += 1\n", " dfs += [df]" ] }, { "cell_type": "code", "execution_count": 10, "id": "collectible-seeking", "metadata": {}, "outputs": [], "source": [ "df = pd.concat(dfs)\n", "df.to_csv(\"../data/sim_runs.csv\", index=False, index_label=False)" ] }, { "cell_type": "raw", "id": "821bbaea-4c87-4a1b-8cdb-c8039d4ba5de", "metadata": {}, "source": [ "Dynamical Analysis of the EIP-1559 Ethereum Fee Market (code)" ] }, { "cell_type": "raw", "id": "2de36b27-ec2f-4e32-89c4-58afd654e44d", "metadata": {}, "source": [] }, { "cell_type": "raw", "id": "87e1778d-a334-438f-9575-4614799eb9cd", "metadata": {}, "source": [ "// Authors\n", "let authorData = [\"barnabe\"];" ] }, { "cell_type": "raw", "id": "0a608d6e-76cf-4021-908c-1708cee53059", "metadata": {}, "source": [] } ], "metadata": { "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" } }, "nbformat": 4, "nbformat_minor": 5 }