All writing

What a Runtime Can Name Before It Runs

· 14 min read

  • architecture
  • evm
  • solana
  • state

What a Runtime Can Name Before It Runs

Two calls carrying byte identical calldata to the same contract wrote two different storage slots, because another contract's state changed between them. Thirty one lines of Solidity, a test suite that runs against them in under a millisecond, and the specimen is printed below.

Solana would have named the account that write landed in before the transaction was scheduled. The account list arrives with the transaction, one way or another, and the runtime holds it before scheduling.

That difference reads as one of speed. Every specification and every source file quoted here was read on 2026-08-24, in the primary itself and never in a description of it, and the piece was written that day. Nothing in it was measured on either mainnet and no benchmark in it was reproduced. The speed question is somebody else's, and the best measurement I found runs the other way.

What survives is narrower. A runtime can put a price or a ceiling only on something it can name before it runs, and where a chain keeps its state is what decides whether the thing being priced has a name at that moment.

A scheduler waits on names, and the EVM has none to hand it

The Ethereum Virtual Machine keeps contract state in a key value map belonging to the contract, and the key is whatever the code computes. A Solidity mapping puts the value for key k at the hash of k concatenated with the slot the mapping was declared at, so the address of a write is a function of the key rather than a fixed position in the layout. the Solidity documentation, layout of state variables in storage Feed that key from another contract's storage, from a return value, from an oracle read, and the address settles at execution.

Role: a reduced executable model, complete and compilable as printed.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
 
/// A contract whose state is changed by an ordinary, independent transaction.
contract Registry {
    uint256 public shard;
 
    function setShard(uint256 s) external {
        shard = s;
    }
}
 
/// A contract whose written storage slot is decided by another contract's state
/// at execution time rather than by the calldata it was called with.
contract Ledger {
    Registry private immutable REG;
    mapping(uint256 => uint256) private balances; // declared at slot 0
 
    constructor(Registry r) {
        REG = r;
    }
 
    function credit(uint256 account, uint256 amount) external {
        balances[account ^ REG.shard()] += amount;
    }
 
    /// The layout witness: where the mapping puts the value for a given key.
    function slotFor(uint256 key) external pure returns (bytes32) {
        return keccak256(abi.encode(key, uint256(0)));
    }
}

Ledger.credit builds its key out of the caller's argument and a value it reads from Registry, so two calls with identical bytes land on different slots once anybody touches the registry. That write is an ordinary transaction from a sender who never heard of the ledger. Neither of those two senders know about the other. No static reading of the calldata recovers the slot, and no access list built at signing time can be honest, because the value it turns on moves before inclusion.

Ethereum's answer is to charge for finding out. In the Osaka definition of the Ethereum execution specification a cold storage access costs 2,100 gas, a cold account access costs 2,600, and a warm one costs 100. the execution specification, Osaka gas constants The Berlin definition of the same specification carries the identical pair. A second specimen in the same harness prices the discovery directly: on solc 0.8.26 with the optimizer off, the first read of a storage slot cost 2,000 gas more than the second read of the same slot inside the same call.

The pricing has a named cause. The specification that raised the first touch price says why in its own motivation: in the 2016 Shanghai denial of service attacks, one of the strategies that kept working was to send transactions that touch a large number of accounts. EIP-2929, gas cost increases for state access opcodes Charging more for the first touch is what a runtime does when it cannot refuse in advance. It never sees the request coming, so it bills the discovery.

The strongest case says the declaration buys nothing

The best argument against everything below is that the list is unnecessary.

Optimistic parallel execution takes the opposite bet: run the block against a preset order, watch which transactions read something an earlier one later wrote, and rerun those. Block-STM detects dependencies during speculative execution rather than being told them. The paper reports it running in production at Aptos. Optimistic execution asks the sender for nothing: no wallet change, no client change, no new composition rule.

It suits the traffic that exists. Ethereum's own proposal for access lists at the level of the block reports that between 60 and 80 percent of transactions already touch disjoint storage slots. EIP-7928, block level access lists Speculation against that distribution commits almost every time.

Then the measurement, and it costs me the headline. They compared their engine against Bohm, which assumes the write sets are known in advance. One comparison, three readings. On the one head to head in that paper, the optimistic engine came out comparable to a deterministic engine that had been handed perfect write sets. Block-STM, arXiv 2203.06871 The authors say the deterministic engine's advantage rests on that perfect information. They also record that without one construction overhead inside their own implementation of it, the deterministic engine would have come out slightly ahead. The same comparison records the optimistic engine significantly ahead at 32 threads on the smaller block size.

Read that with the sign flipped. A predeclared write set, supplied free and never wrong, came out close enough that the winner changes with the thread count and with one implementation choice. This article does not establish which state model executes a block faster, and every throughput figure it touches belongs to somebody else's benchmark.

Optimistic execution reaches the same parallelism without asking the sender for anything, and in the head to head its own authors ran it matched an engine handed perfect write sets. So if parallelism is why you came to the account model, it is overhead and you can stop here.

What the declaration buys is a meter with an address on it

The function that locks accounts says in its own comment that it exists so that two threads never modify one account's state at the same time. the agave validator, the account locking function The list it locks is taken off the transaction itself, from the message's own account keys and their writable flags. A newer transaction format lets the sender give the list by reference instead, and the runtime resolves those references against chain state while it loads the transaction, still before it schedules anything. the agave validator, address lookup table resolution The lock layer's answer to a held account is an error returned for that transaction, and what happens to it next is a scheduling policy above the lock rather than a property of the lock.

The half that matters is one file over, a constant with a comment on it. The runtime caps the compute units any single writable account may consume inside one block at 24,000,000, and the comment above the constant gives the reason as stopping too many transactions writing the same account and reducing the block's parallelism. the agave validator, the block cost limits The block as a whole is allowed 60,000,000. One account is therefore allowed 40 percent of the block.

That ceiling is an object nobody on Ethereum can build: a limit on one named piece of state, enforced while a block is packed, before any of it runs. It is possible only because the runtime knows which account each transaction writes without running it.

Solana                                    Ethereum
 
transaction carries                       transaction carries
  an account list, each entry               to, value, data
  marked writable or not
        |                                         |
        v                                         v
  SCHEDULER                                   ADMITTED
    takes a lock on every                         |
    writable entry                                v
    checks this block's                       EXECUTION
    ceiling for that entry                        |  finds each slot as it
        |                                         |  reaches it
        v                                         v
   EXECUTION                                  CHARGED, cold then warm
 
the meter sits BEFORE execution            the meter sits INSIDE execution
and is attached to a named account         and is attached to the transaction

Ethereum has the mechanism and lacks the knowledge. The optional access list an Ethereum transaction may carry is a hint and never a restriction: the specification that introduced it says accesses outside the list are possible, and only more expensive. EIP-2930, optional access lists Declaring one storage key in that list costs 1,900 gas. The address it sits under costs another 2,400, added once per access list entry. The address charge is added for every entry in the list, before the per key charge, so a key cannot be declared without one. the execution specification, Osaka intrinsic gas And the specification that raised the first touch price already warms the sender and the contract being called before execution starts. So declaring one slot on the contract you are calling costs 2,400 for an address that was already warm, 1,900 for the key and 100 for the warm read, which is 4,400 against the 2,100 the same read costs undeclared, a loss of 2,300 gas. The list does better on an address you would have touched cold anyway. On a cold address the same declaration costs 2,400 and 1,900 up front and two warm reads of 100, which is 4,500 against the 4,700 an undeclared cold address at 2,600 and a cold slot at 2,100 cost, a saving of 200. That is the best case.

The newest fork definition goes further. In the Amsterdam definition the same item cost is written as the cold access cost minus the warm access cost. the execution specification, Amsterdam gas constants In that same definition a cold storage access still costs 2,100, a cold account access costs 3,000, and a warm access still costs 100. Under that rule the same declaration costs 2,900 for the address and 2,000 for the key, and the read is still 100, which is 5,000 against 2,100, a loss of 2,900.

Ethereum's own proposal for access lists at the level of the block opens on the constraint: execution cannot be parallelized unless the addresses and the storage slots are known ahead of time. EIP-7928, block level access lists Its own abstract says what that buys: reads from disk in parallel, transactions validated in parallel, the state root computed in parallel, and state updates that need no execution at all. A speculative engine gets none of those, which is how a flat cannot stands over one that does. Its fix is to have the block producer publish the list after the fact, which is right for what it aims at and never reaches admission. The list does not exist until the block has run, and by then who got in, in what order, and at what price has been answered.

The one place Ethereum prices a second resource shows the rule. Ethereum's blob specification gives blobs a gas of their own, priced independently of execution gas and targeted by its own rule. EIP-4844, shard blob transactions A blob count binds: a transaction cannot use more blobs than it declared. A declared storage key binds nothing, so the count a transaction offers there can never be the basis of a market.

Solana takes the write set off the transaction and turns it into locks and a ceiling on one named account, while Ethereum finds the write set during execution and can charge nobody but the transaction that found it.

Only one of these two runtimes has my own settlement work inside it. The settle itself is ERC-6909 internal balance accounting on warm slots with matching amortized across the batch, no ERC-20 external calls on the hot path. What carries across is the shape of that decision; what does not carry is any measurement of what a declared account list costs to run. Both halves of this piece rest mostly on documents I read, and the difference is that one of them has a claim I could run and the other has none. Every one of those choices makes the discovery price small: balances inside one contract, touched after the batch has warmed them, no external call on the hot path. On a runtime scheduling from a declared account list they land elsewhere. One contract holding every balance concentrates the writes onto one account, and what you optimised away returns as a lock and a share of its ceiling.

Put a price on a resource only where you can name it before you run, because a price with nothing to attach to lands on the whole block.

The same line falls out of a problem with no chain. An HTTP service can rate limit per API key, because the key is in the header and the gateway reads it before the handler. It can only rate limit per row where the row is named in the request. Where the handler decides which rows get touched, the door has nothing to key on. The header is the account list; the rows are the storage slots.

Somebody pays for the name, and it is not the account the ceiling protects

Four parties carry the declaration.

The program author pays first. Every account an instruction reads or writes has to be enumerated by whoever builds the transaction, and a list that misses one fails rather than costing more. The exception is the runtime's own clocks and schedules, which a program can read through a syscall out of a cache the runtime already holds, paying compute rather than naming an account.

The wallet pays next, resolving the list before signing from state that can move before inclusion, and a stale list fails rather than quietly costing more.

The team running a hot program pays in throughput it does not control, against a constant set by software it does not ship. Every remedy is inside its own program: mark the account read only where the instruction does not write it, spend fewer compute units, or split it.

The Ethereum block builder pays on the other route, because a block level list is produced by whoever ran the block.

There is a fifth bill of this shape. My own redesign of a settlement path is mine, design and implementation and measurement, and rebalancing makes the per trade cost a distribution rather than a constant. Anybody sizing a gas limit against it sizes against the tail, which is a cost I put on them.

Nothing I retained prices any of the four, so none carries a figure here.

One hot account makes the whole argument moot

Run the discriminator on the case it gets wrong.

Take a workload whose writes land on one account: a single market, a single mint, a single global counter. The object has a name, it is on every transaction, and the scheduling is perfect. What it schedules is a queue. They all want the same write lock, so its ceiling is a hard stop rather than a price. There is nothing left to parallelise.

The proposal that argues for lifting that ceiling describes the capacity it is unlocking as serial. SIMD-0306, raising the writable account limit The same proposal says the network was consistently hitting the current 12M CU cap. That figure and the 24,000,000 above are one ceiling at two moments: the proposal's own table reads 12M against a 50M block, the validator source today reads 24,000,000 against 60,000,000, which is the 40 percent this proposal argues for, and the proposal is still at status Review, so something other than its adoption put that ratio there. Raising the ceiling on serial execution is the honest thing to raise once the work has stopped being divisible.

Once a workload's writes concentrate on one account, the ceiling on that account is the binding constraint and the state model has stopped being the variable worth arguing about. Splitting it is a program design problem and this piece is about the runtime underneath, so the move gets named and left: shard the account, publish the addresses, let the client pick.

What this does not settle

The analysis stops applying where nothing in your workload is worth a price of its own. If every charge you want to levy falls on the whole chain, the declaration is overhead and the optimistic route wins. It examines no production throughput on either chain. The head to head it leans on is one benchmark, by its own authors on their own hardware, with the competing engine handed information those authors call unrealistic in a blockchain.

The question to carry into either design is which of the things you want to charge for has a name when the block is packed. On one runtime the answer is written on the transaction. On the other it is written nowhere until the block has run.


Abdel KIARI

I’ve owned the EVM side of a DeFi protocol. I redesigned its architecture from scratch and built the Solidity infrastructure end to end.

Always happy to talk about interesting opportunities

abdel.kiari@gmail.com