<jupyter_start><jupyter_text>Model assumptions:- All transactions use 500,000 gas, block limit is 25,000,000 gas (i.e., 50 transactions).- Simulation lasts for 200 steps.- Each step, a random number of users $N_t$ is drawn from a Poisson distribution of size 100.- User value is drawn from uniform distribution between 0 and 20 Gwei per gas unit.- Users bid the miner fee as premium, = 1 Gwei.- Excess gas issued starts from an arbitrary value set to 3,600 Mgas (~0.06 Gwei implied basefee)- Implied basefee in a block providing `gas_used`, given `excess_gas_issued` is$$ b = \frac{\text{ethqty}(\text{excess gas issued} + \text{gas used}) - \text{ethqty}(\text{excess gas issued})}{\text{gas used}} $$where$$ \text{ethqty}(g) = \exp \Big( \frac{g}{\text{target gas used} \times \text{adjustment quotient}} \Big) $$  Transaction pool- Evicts transactions whenever there are more than 500 pending in the queue (reminder: we set transaction such that a full block = 50 transactions)- Given the current `excess_gas_issued`, miner computes their revenue should they include 0 transaction, 1 transaction, 2 transactions, ..., 50 transactions. Number of transactions included is set so that max revenue is achieved.<jupyter_code>MAX_TX_POOL = 500
MIN_PREMIUM = 1e9

class TxPoolLimit(TxPool):
    
    def __init__(self, max_txs=MAX_TX_POOL, min_premium=MIN_PREMIUM, **kwargs):
        super().__init__(**kwargs)
        self.max_txs = max_txs
        self.min_premium = MIN_PREMIUM
    
    def add_txs(self, txs: Sequence[Tx1559], env: dict) -> Sequence[Tx1559]:
        for tx in txs:
            self.txs[tx.tx_hash] = tx
                    
        if self.pool_length() > self.max_txs:
            sorted_txs = sorted(self.txs.values(), key = lambda tx: -tx.tip(env))
            self.empty_pool()
            self.add_txs(sorted_txs[0:self.max_txs], env)
            return sorted_txs[self.max_txs:]
        
        return []

class TxPool1559(TxPoolLimit):
    
    def select_transactions(self, env, user_pool=None, rng=rng) -> Sequence[Tx1559]:
        # Miner side
        max_tx_in_block = int(constants["MAX_GAS_EIP1559"] / constants["SIMPLE_TRANSACTION_GAS"])

        valid_txs = [tx for tx in self.txs.values() if tx.is_valid(env) and tx.tip(env) >= self.min_premium]
        rng.shuffle(valid_txs)

        sorted_valid_demand = sorted(
            valid_txs,
            key = lambda tx: -tx.tip(env)
        )
        selected_txs = sorted_valid_demand[0:max_tx_in_block]

        return selected_txs

class TxPoolAMMImplied(TxPoolLimit):
    
    def select_transactions(self, env, user_pool=None, rng=rng) -> Sequence[Tx1559]:
        # Miner side
        max_tx_in_block = int(constants["MAX_GAS_EIP1559"] / constants["SIMPLE_TRANSACTION_GAS"])
        excess_gas_issued = env["excess_gas_issued"]
        revenues = [0]
        
        for i in range(1, max_tx_in_block + 1):        
            implied_basefee = eth_qty(excess_gas_issued + i * constants["SIMPLE_TRANSACTION_GAS"]) - eth_qty(excess_gas_issued)
            implied_basefee = implied_basefee / (i * constants["SIMPLE_TRANSACTION_GAS"])
            valid_txs = [tx for tx in self.txs.values() if tx.is_valid({ "basefee": implied_basefee }) and tx.tip({ "basefee": implied_basefee }) >= self.min_premium]
            sorted_valid_demand = sorted(
                valid_txs,
                key = lambda tx: -tx.tip({ "basefee": implied_basefee })
            )
            revenues += [sum([tx.tip({ "basefee": implied_basefee }) for tx in sorted_valid_demand[0:(i-1)]])]
        
        max_revenue = max(revenues)
        
        if max_revenue == 0:
            return []
        
        opt_n_tx = len(revenues) - 1 - revenues[::-1].index(max_revenue)
        implied_basefee = eth_qty(excess_gas_issued + opt_n_tx * constants["SIMPLE_TRANSACTION_GAS"]) - eth_qty(excess_gas_issued)
        implied_basefee = implied_basefee / (opt_n_tx * constants["SIMPLE_TRANSACTION_GAS"])
        valid_txs = [tx for tx in self.txs.values() if tx.is_valid({ "basefee": implied_basefee }) and tx.tip({ "basefee": implied_basefee }) >= self.min_premium]
        sorted_valid_demand = sorted(
            valid_txs,
            key = lambda tx: -tx.tip({ "basefee": implied_basefee })
        )
        selected_txs = sorted_valid_demand[0:opt_n_tx]

        return selected_txs<jupyter_output><empty_output><jupyter_text>Main simulation loop<jupyter_code>def simulate_amm_implied(demand_scenario, txpool, UserClass, extra_metrics = None, rng = rng, silent=False):
    # Instantiate a couple of things
    chain = Chain()
    metrics = []
    user_pool = UserPool()
    
    # `env` is the "environment" of the simulation
    env = {
        "basefee": 0,
        "current_block": None,
        "excess_gas_issued": 3625 * 1e6,
    }

    for t in tqdm(range(len(demand_scenario)), disable=silent, desc="simulation loop", leave=False):
        if demand_scenario[t] > 5 * MAX_TX_POOL:
            break
        
        # Sets current block
        env["current_block"] = t
        
        # Reset the random number generator with new seed to generate users with same values across runs
        rng = np.random.default_rng((2 ** t) * (3 ** 1))
        
        ### SIMULATION ###

        # We return some demand which on expectation yields `demand_scenario[t]` new users per round
        users = spawn_poisson_demand(t, demand_scenario[t], User1559, rng=rng)
        
        # Add new users to the pool
        # We query each new user with the current basefee value
        # Users either return a transaction or None if they prefer to balk
        decided_txs = user_pool.decide_transactions(users, env)

        # New transactions are added to the transaction pool
        # `evicted_txs` holds the transactions removed from the pool for lack of space
        pool_limit_evicted_txs = txpool.add_txs(decided_txs, env)

        # The best valid transactions are taken out of the pool for inclusion
        selected_txs = txpool.select_transactions(env, rng = rng)
        
        txpool.remove_txs([tx.tx_hash for tx in selected_txs])

        # We create a block with these transactions
        block = BlockAMMImplied(
            txs = selected_txs, parent_hash = chain.current_head,
            height = t, excess_gas_issued = env["excess_gas_issued"]
        )
        
        env["excess_gas_issued"] = max(0, env["excess_gas_issued"] + block.gas_used() - constants["TARGET_GAS_USED"])
        env["basefee"] = eth_qty(env["excess_gas_issued"])
        env["basefee"] = env["basefee"] / constants["BASEFEE_MAX_CHANGE_DENOMINATOR"] / constants["TARGET_GAS_USED"]
        
        # The block is added to the chain
        chain.add_block(block)
        
        ### METRICS ###                
        row_metrics = {
            "block": t,
            "users": len(users),
            "decided_txs": len(decided_txs),
            "pool_limit_evictions": len(pool_limit_evicted_txs),
            "included_txs": len(selected_txs),
            "basefee": env["basefee"] / 1e9, # to Gwei
            "excess_gas_issued": env["excess_gas_issued"] / 1e6, # in Mgas
            "pool_length": txpool.pool_length(),
            "gas_used": block.gas_used(),
        }
        
        if not extra_metrics is None:
            row_metrics = {
                **row_metrics,
                **extra_metrics(env, users, user_pool, txpool),
            }
        
        metrics.append(row_metrics)

    return (pd.DataFrame(metrics), user_pool, chain)
%%capture
demand_scenario = [100 for i in range(200)]
txpool = TxPoolAMMImplied()
(df, user_pool, chain) = simulate_amm_implied(demand_scenario, txpool, User1559)<jupyter_output><empty_output><jupyter_text>Plots Basefee over time<jupyter_code>df["basefee"].plot()<jupyter_output><empty_output><jupyter_text>Transaction pool over time<jupyter_code>df["pool_length"].plot()<jupyter_output><empty_output><jupyter_text>Excess gas issued over time<jupyter_code>df["excess_gas_issued"].plot()<jupyter_output><empty_output><jupyter_text>Oscillations revisited- Users have value between 10 and 11 Gwei per gas unit.- 1000 users show up each step.<jupyter_code>class NarrowUser(User1559):
    def __init__(self, wakeup_block, **kwargs):
        super().__init__(wakeup_block, cost_per_unit = 0, **kwargs)
        self.value = self.rng.uniform(low=10, high=11) * (10 ** 9)<jupyter_output><empty_output><jupyter_text>With AMM design<jupyter_code>%%capture
demand_scenario = [1000 for i in range(200)]
txpool = TxPoolAMMImplied()
(df_amm, _, _) = simulate_amm_implied(demand_scenario, txpool, NarrowUser)<jupyter_output><empty_output><jupyter_text>Basefee over time<jupyter_code>df_amm["basefee"].plot()<jupyter_output><empty_output><jupyter_text>Gas used per block<jupyter_code>df_amm["gas_used"].plot()<jupyter_output><empty_output><jupyter_text>With classic 1559<jupyter_code>def simulate_1559(demand_scenario, txpool, UserClass, extra_metrics = None, rng = rng, silent=False):
    # Instantiate a couple of things
    chain = Chain()
    metrics = []
    user_pool = UserPool()
    
    # `env` is the "environment" of the simulation
    env = {
        "basefee": 0.06 * 1e9,
        "current_block": None,
    }

    for t in tqdm(range(len(demand_scenario)), disable=silent, desc="simulation loop", leave=False):
        
        # Sets current block
        env["current_block"] = t
        
        # Reset the random number generator with new seed to generate users with same values across runs
        rng = np.random.default_rng((2 ** t) * (3 ** 1))
        
        ### SIMULATION ###

        # We return some demand which on expectation yields `demand_scenario[t]` new users per round
        users = spawn_poisson_demand(t, demand_scenario[t], User1559, rng=rng)
        
        # Add new users to the pool
        # We query each new user with the current basefee value
        # Users either return a transaction or None if they prefer to balk
        decided_txs = user_pool.decide_transactions(users, env)

        # New transactions are added to the transaction pool
        # `evicted_txs` holds the transactions removed from the pool for lack of space
        pool_limit_evicted_txs = txpool.add_txs(decided_txs, env)

        # The best valid transactions are taken out of the pool for inclusion
        selected_txs = txpool.select_transactions(env, rng = rng)
        
        txpool.remove_txs([tx.tx_hash for tx in selected_txs])

        # We create a block with these transactions
        block = Block1559(
            txs = selected_txs, parent_hash = chain.current_head,
            height = t, basefee = env["basefee"]
        )
        
        # The block is added to the chain
        chain.add_block(block)
        
        ### METRICS ###                
        row_metrics = {
            "block": t,
            "users": len(users),
            "decided_txs": len(decided_txs),
            "pool_limit_evictions": len(pool_limit_evicted_txs),
            "included_txs": len(selected_txs),
            "basefee": env["basefee"] / 1e9, # to Gwei
            "pool_length": txpool.pool_length(),
            "gas_used": block.gas_used(),
        }
        
        if not extra_metrics is None:
            row_metrics = {
                **row_metrics,
                **extra_metrics(env, users, user_pool, txpool),
            }
        
        metrics.append(row_metrics)
        
        # Finally, basefee is updated and a new round starts
        env["basefee"] = update_basefee(block, env["basefee"])

    return (pd.DataFrame(metrics), user_pool, chain)
%%capture
demand_scenario = [1000 for i in range(200)]
txpool = TxPool1559()
(df_1559, _, _) = simulate_1559(demand_scenario, txpool, NarrowUser)<jupyter_output><empty_output><jupyter_text>Basefee over time<jupyter_code>df_1559["basefee"].plot()<jupyter_output><empty_output><jupyter_text>Gas used per block<jupyter_code>df_1559["gas_used"].plot()<jupyter_output><empty_output><jupyter_text>Oscillations do seem more violent in the classic 1559 case, though they do not completely disappear with the AMM design.<jupyter_code>Make EIP 1559 more like an AMM curve
Exploring https://ethresear.ch/t/make-eip-1559-more-like-an-amm-curve/9082
// References + footnotes

// Authors
let authorData = ["barnabe"];<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10460318744438152, 'fraction_numerical': 0.017231615565083732, 'mean_word_length': 2.9533098816757275, 'pattern_counts': {'":': 29, '<': 53, '<?xml version=': 0, '>': 61, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '13', 'dir': 'jupyter-structured-clean-dedup', 'id': '212961', 'license': 'MIT', 'n_tokens_mistral': 4159, 'n_tokens_neox': 3934, 'n_words': 963, 'path': 'notebooks/amm_eip1559.ipynb', 'repo_name': 'SHSR2001/abm1559', 'size': '12361'}