Skip to content

Commit 11009a5

Browse files
committed
Lock in storage changes before transaction starts
Currently, because we don't do this, there is a bug in EIP-2200, net gas metering. When looking up the "original value", the EVM must get the storage value at the beginning of the transaction, not the beginning of the block. So we lock in changes at the beginning of every transaction, to make sure we don't read values from older transactions, when reading the original value.
1 parent f25d209 commit 11009a5

File tree

6 files changed

+47
-4
lines changed

6 files changed

+47
-4
lines changed

eth/abc.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1573,6 +1573,16 @@ def commit(self, checkpoint: JournalDBCheckpoint) -> None:
15731573
"""
15741574
...
15751575

1576+
@abstractmethod
1577+
def lock_changes(self) -> None:
1578+
"""
1579+
Locks in changes to storage, typically just as a transaction starts.
1580+
1581+
This is used, for example, to look up the storage value from the start
1582+
of the transaction, when calculating gas costs in EIP-2200: net gas metering.
1583+
"""
1584+
...
1585+
15761586
@abstractmethod
15771587
def make_storage_root(self) -> None:
15781588
"""
@@ -2153,6 +2163,16 @@ def commit(self, snapshot: Tuple[Hash32, UUID]) -> None:
21532163
"""
21542164
...
21552165

2166+
@abstractmethod
2167+
def lock_changes(self) -> None:
2168+
"""
2169+
Locks in all changes to state, typically just as a transaction starts.
2170+
2171+
This is used, for example, to look up the storage value from the start
2172+
of the transaction, when calculating gas costs in EIP-2200: net gas metering.
2173+
"""
2174+
...
2175+
21562176
@abstractmethod
21572177
def persist(self) -> None:
21582178
"""

eth/db/account.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,10 @@ def commit(self, checkpoint: JournalDBCheckpoint) -> None:
370370
for _, store in self._dirty_account_stores():
371371
store.commit(checkpoint)
372372

373+
def lock_changes(self) -> None:
374+
for _, store in self._dirty_account_stores():
375+
store.lock_changes()
376+
373377
def make_state_root(self) -> Hash32:
374378
for _, store in self._dirty_account_stores():
375379
store.make_storage_root()

eth/db/storage.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def __init__(self, db: AtomicDatabaseAPI, storage_root: Hash32, address: Address
157157
158158
.. code::
159159
160-
db -> _storage_lookup -> _storage_cache -> _journal_storage
160+
db -> _storage_lookup -> _storage_cache -> _locked_changes -> _journal_storage
161161
162162
db is the raw database, we can assume it hits disk when written to.
163163
Keys are stored as node hashes and rlp-encoded node values.
@@ -174,6 +174,10 @@ def __init__(self, db: AtomicDatabaseAPI, storage_root: Hash32, address: Address
174174
after a state root change. Otherwise, you will see data since the last
175175
storage root was calculated.
176176
177+
_locked_changes is a batch database that includes only those values that are
178+
un-revertable in the EVM. Currently, that means changes that completed in a
179+
previous transaction.
180+
177181
Journaling batches writes at the _journal_storage layer, until persist is called.
178182
It manages all the checkpointing and rollbacks that happen during EVM execution.
179183
@@ -183,11 +187,12 @@ def __init__(self, db: AtomicDatabaseAPI, storage_root: Hash32, address: Address
183187
self._address = address
184188
self._storage_lookup = StorageLookup(db, storage_root, address)
185189
self._storage_cache = CacheDB(self._storage_lookup)
186-
self._journal_storage = JournalDB(self._storage_cache)
190+
self._locked_changes = BatchDB(self._storage_cache)
191+
self._journal_storage = JournalDB(self._locked_changes)
187192

188193
def get(self, slot: int, from_journal: bool=True) -> int:
189194
key = int_to_big_endian(slot)
190-
lookup_db = self._journal_storage if from_journal else self._storage_cache
195+
lookup_db = self._journal_storage if from_journal else self._locked_changes
191196
try:
192197
encoded_value = lookup_db[key]
193198
except MissingStorageTrieNode:
@@ -237,9 +242,13 @@ def commit(self, checkpoint: JournalDBCheckpoint) -> None:
237242
# then flatten all changes, without persisting
238243
self._journal_storage.flatten()
239244

240-
def make_storage_root(self) -> None:
245+
def lock_changes(self) -> None:
241246
self._journal_storage.persist()
242247

248+
def make_storage_root(self) -> None:
249+
self.lock_changes()
250+
self._locked_changes.commit(apply_deletes=True)
251+
243252
def _validate_flushed(self) -> None:
244253
"""
245254
Will raise an exception if there are some changes made since the last persist.

eth/vm/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ def apply_transaction(self,
148148
transaction: SignedTransactionAPI
149149
) -> Tuple[ReceiptAPI, ComputationAPI]:
150150
self.validate_transaction_against_header(header, transaction)
151+
152+
# Mark current state as un-revertable, since new transaction is starting...
153+
self.state.lock_changes()
154+
151155
computation = self.state.apply_transaction(transaction)
152156
receipt = self.make_receipt(header, transaction, computation, self.state)
153157
self.validate_receipt(receipt)

eth/vm/state.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ def commit(self, snapshot: Tuple[Hash32, UUID]) -> None:
175175
_, account_snapshot = snapshot
176176
self._account_db.commit(account_snapshot)
177177

178+
def lock_changes(self) -> None:
179+
self._account_db.lock_changes()
180+
178181
def persist(self) -> None:
179182
self._account_db.persist()
180183

newsfragments/1893.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix for net gas metering (EIP-2200) in Istanbul. The "original value" used to calculate gas
2+
costs was incorrectly accessing the value at the start of the block, instead of the start of the
3+
transaction.

0 commit comments

Comments
 (0)