diff --git a/eth/db/account.py b/eth/db/account.py index 4acc7a3df3..0262e74305 100644 --- a/eth/db/account.py +++ b/eth/db/account.py @@ -19,7 +19,9 @@ ) from eth_utils import ( + big_endian_to_int, encode_hex, + int_to_big_endian, to_checksum_address, to_tuple, ValidationError, @@ -41,6 +43,9 @@ from eth.db.batch import ( BatchDB, ) +from eth.db.block_diff import ( + BlockDiff, +) from eth.db.cache import ( CacheDB, ) @@ -52,6 +57,7 @@ ) from eth.db.storage import ( AccountStorageDB, + StorageLookup, ) from eth.db.typing import ( JournalDBCheckpoint, @@ -260,6 +266,9 @@ def __init__(self, db: BaseAtomicDB, state_root: Hash32=BLANK_ROOT_HASH) -> None self._dirty_accounts: Set[Address] = set() self._root_hash_at_last_persist = state_root + self._dirty_account_rlps: Set[Address] = set() + self._deleted_accounts: Set[Address] = set() + @property def state_root(self) -> Hash32: return self._trie.root_hash @@ -436,6 +445,7 @@ def delete_account(self, address: Address) -> None: del self._journaltrie[address] self._wipe_storage(address) + self._deleted_accounts.add(address) def account_exists(self, address: Address) -> bool: validate_canonical_address(address, title="Storage Address") @@ -484,6 +494,8 @@ def _set_account(self, address: Address, account: Account) -> None: rlp_account = rlp.encode(account, sedes=Account) self._journaltrie[address] = rlp_account + self._dirty_account_rlps.add(address) + # # Record and discard API # @@ -555,6 +567,8 @@ def persist(self) -> None: # reset local storage trackers self._account_stores = {} self._dirty_accounts = set() + self._dirty_account_rlps = set() + self._deleted_accounts = set() # persist accounts self._validate_generated_root() @@ -565,6 +579,105 @@ def persist(self) -> None: self._batchdb.commit_to(write_batch, apply_deletes=False) self._root_hash_at_last_persist = new_root_hash + def persist_returning_block_diff(self) -> BlockDiff: + """ + Persists, including a diff which can be used to unwind/replay the changes this block makes. + """ + + block_diff = BlockDiff() + + # 1. Grab all the changed accounts and their previous values + + # pre-Byzantium make_storage_root is called at the end of every transaction, and + # it blows away all the changes. Create an old_trie here so we can peer into the + # state as it was at the beginning of the block. + + old_trie = CacheDB(HashTrie(HexaryTrie( + self._raw_store_db, self._root_hash_at_last_persist, prune=False + ))) + + for deleted_address in self._deleted_accounts: + # TODO: this might raise a KeyError + old_value = old_trie[deleted_address] + block_diff.set_account_changed(deleted_address, old_value, b'') + + for address in self._dirty_account_rlps: + old_value = old_trie[address] + new_value = self._get_encoded_account(address, from_journal=True) + block_diff.set_account_changed(address, old_value, new_value) + + # 2. Grab all the changed storage items and their previous values. + dirty_stores = tuple(self._dirty_account_stores()) + for address, store in dirty_stores: + diff = store.diff() + + for key in diff.deleted_keys(): + slot = big_endian_to_int(key) + current_slot_value = store.get(slot) + current_slot_value_bytes = int_to_big_endian(current_slot_value) + # TODO: Is b'' a valid value for a storage slot? 0 might be better + # TODO: this line is untested + block_diff.set_storage_changed(address, slot, current_slot_value_bytes, b'') + + encoded_account = old_trie[address] + if encoded_account: + old_account = rlp.decode(encoded_account, sedes=Account) + else: + old_account = Account() + fresh_store = StorageLookup( + self._raw_store_db, + old_account.storage_root, + address + ) + + for key, new_value in diff.pending_items(): + slot = big_endian_to_int(key) + + # make a new StorageLookup because, pre-Byzantium, make_state_root is + # called at the end of every transaction, and making the state root blows + # away all changes. If we were to ask the store for the old value it would + # tell us the state as of the beginning of the last txn, not the state as + # of the beginnig of the block. + + old_value_bytes = fresh_store.get(key) + + block_diff.set_storage_changed(address, slot, old_value_bytes, new_value) + + old_account_values: Dict[Address, bytes] = dict() + for address, _ in dirty_stores: + old_account_values[address] = self._get_encoded_account(address, from_journal=False) + + # 3. Persist! + self.persist() + + # 4. Grab the new storage roots + for address, _store in dirty_stores: + old_account_value = old_account_values[address] + new_account_value = self._get_encoded_account(address, from_journal=False) + block_diff.set_account_changed(address, old_account_value, new_account_value) + + # 5. return the block diff + return block_diff + + def _changed_accounts(self) -> DBDiff: + """ + Returns all the accounts which will be written to the db when persist() is called. + + Careful! If some storage items have changed then the storage roots for some accounts + should also change but those accounts will not show up here unless something else about + them also changed. + """ + return self._journaltrie.diff() + + def _changed_storage_items(self) -> Dict[Address, DBDiff]: + """ + Returns all the storage items which will be written to the db when persist() is called. + """ + return { + address: store.diff() + for address, store in self._dirty_account_stores() + } + def _validate_generated_root(self) -> None: db_diff = self._journaldb.diff() if len(db_diff): diff --git a/eth/db/block_diff.py b/eth/db/block_diff.py new file mode 100644 index 0000000000..f5c3667df8 --- /dev/null +++ b/eth/db/block_diff.py @@ -0,0 +1,133 @@ +from collections import defaultdict +from typing import ( + Dict, + Iterable, + Optional, + Set, + Tuple, +) + +from eth_typing import ( + Address, + Hash32, +) +from eth_utils import ( + big_endian_to_int, + to_tuple +) +import rlp + +from eth.db.backends.base import BaseDB +from eth.db.schema import SchemaTurbo +from eth.rlp.accounts import Account + + +""" +TODO: Decide on the best interface for returning changes: +- diff.get_slot_change() -> [old, new] +- diff.get_slot_change(new=FAlse) -> old +- diff.get_slot_change(kind=BlockDiff.OLD) -> old +- diff.get_old_slot_value() & diff.get_new_slot_value() +""" + + +class BlockDiff: + + def __init__(self) -> None: + self.old_account_values: Dict[Address, Optional[bytes]] = dict() + self.new_account_values: Dict[Address, Optional[bytes]] = dict() + + SLOT_TO_VALUE = Dict[int, bytes] + self.old_storage_items: Dict[Address, SLOT_TO_VALUE] = defaultdict(dict) + self.new_storage_items: Dict[Address, SLOT_TO_VALUE] = defaultdict(dict) + + def set_account_changed(self, address: Address, old_value: bytes, new_value: bytes) -> None: + self.old_account_values[address] = old_value + self.new_account_values[address] = new_value + + def set_storage_changed(self, address: Address, slot: int, + old_value: bytes, new_value: bytes) -> None: + self.old_storage_items[address][slot] = old_value + self.new_storage_items[address][slot] = new_value + + def get_changed_accounts(self) -> Set[Address]: + return set(self.old_account_values.keys()) | set(self.old_storage_items.keys()) + + @to_tuple + def get_changed_storage_items(self) -> Iterable[Tuple[Address, int, bytes, bytes]]: + for address in self.old_storage_items.keys(): + new_items = self.new_storage_items[address] + old_items = self.old_storage_items[address] + for slot in old_items.keys(): + yield address, slot, old_items[slot], new_items[slot] + + def get_changed_slots(self, address: Address) -> Set[int]: + """ + Returns which slots changed for the given account. + """ + if address not in self.old_storage_items.keys(): + return set() + + return set(self.old_storage_items[address].keys()) + + def get_slot_change(self, address: Address, slot: int) -> Tuple[int, int]: + if address not in self.old_storage_items: + raise Exception(f'account {address} did not change') + old_values = self.old_storage_items[address] + + if slot not in old_values: + raise Exception(f"{address}'s slot {slot} did not change") + + new_values = self.new_storage_items[address] + return big_endian_to_int(old_values[slot]), big_endian_to_int(new_values[slot]) + + def get_account(self, address: Address, new: bool = True) -> bytes: + dictionary = self.new_account_values if new else self.old_account_values + return dictionary[address] + + def get_decoded_account(self, address: Address, new: bool = True) -> Optional[Account]: + encoded = self.get_account(address, new) + if encoded == b'': + return None # this means the account used to or currently does not exist + return rlp.decode(encoded, sedes=Account) + + @classmethod + def from_db(cls, db: BaseDB, block_hash: Hash32) -> 'BlockDiff': + """ + KeyError is thrown if a diff was not saved for the provided {block_hash} + """ + + encoded_diff = db[SchemaTurbo.make_block_diff_lookup_key(block_hash)] + diff = rlp.decode(encoded_diff) + + accounts, storage_items = diff + + block_diff = cls() + + for key, old, new in accounts: + block_diff.set_account_changed(key, old, new) + + for key, slot, old, new in storage_items: + decoded_slot = big_endian_to_int(slot) # rlp.encode turns our ints into bytes + block_diff.set_storage_changed(key, decoded_slot, old, new) + + return block_diff + + def write_to(self, db: BaseDB, block_hash: Hash32) -> None: + + # TODO: this should probably verify that the state roots have all been added + + accounts = [ + [address, self.old_account_values[address], self.new_account_values[address]] + for address in self.old_account_values.keys() + ] + + storage_items = self.get_changed_storage_items() + + diff = [ + accounts, + storage_items + ] + + encoded_diff = rlp.encode(diff) + db[SchemaTurbo.make_block_diff_lookup_key(block_hash)] = encoded_diff diff --git a/eth/db/schema.py b/eth/db/schema.py index 1efab2c65b..e506729e8a 100644 --- a/eth/db/schema.py +++ b/eth/db/schema.py @@ -62,6 +62,11 @@ def make_transaction_hash_to_block_lookup_key(transaction_hash: Hash32) -> bytes class SchemaTurbo(SchemaV1): current_schema_lookup_key: bytes = b'current-schema' + _block_diff_prefix = b'block-diff' + + @classmethod + def make_block_diff_lookup_key(cls, block_hash: Hash32) -> bytes: + return cls._block_diff_prefix + b':' + block_hash def get_schema(db: BaseDB) -> Schemas: diff --git a/eth/db/storage.py b/eth/db/storage.py index 3b5d79b065..d881212f25 100644 --- a/eth/db/storage.py +++ b/eth/db/storage.py @@ -31,6 +31,9 @@ from eth.db.cache import ( CacheDB, ) +from eth.db.diff import ( + DBDiff +) from eth.db.journal import ( JournalDB, ) @@ -170,7 +173,7 @@ def __init__(self, db: BaseAtomicDB, storage_root: Hash32, address: Address) -> Keys are stored as node hashes and rlp-encoded node values. _storage_lookup is itself a pair of databases: (BatchDB -> HexaryTrie), - writes to storage lookup *are* immeditaely applied to a trie, generating + writes to storage lookup *are* immediately applied to a trie, generating the appropriate trie nodes and and root hash (via the HexaryTrie). The writes are *not* persisted to db, until _storage_lookup is explicitly instructed to, via :meth:`StorageLookup.commit_to` @@ -191,6 +194,7 @@ def __init__(self, db: BaseAtomicDB, storage_root: Hash32, address: Address) -> self._storage_lookup = StorageLookup(db, storage_root, address) self._storage_cache = CacheDB(self._storage_lookup) self._journal_storage = JournalDB(self._storage_cache) + self._changes_since_last_persist = JournalDB(dict()) def get(self, slot: int, from_journal: bool=True) -> int: key = int_to_big_endian(slot) @@ -211,8 +215,15 @@ def set(self, slot: int, value: int) -> None: key = int_to_big_endian(slot) if value: self._journal_storage[key] = rlp.encode(value) + self._changes_since_last_persist[key] = rlp.encode(value) else: del self._journal_storage[key] + try: + del self._changes_since_last_persist[key] + except KeyError: + self._changes_since_last_persist[key] = b'' + del self._changes_since_last_persist[key] + def delete(self) -> None: self.logger.debug2( @@ -221,28 +232,34 @@ def delete(self) -> None: keccak(self._address).hex(), ) self._journal_storage.clear() + self._changes_since_last_persist.clear() self._storage_cache.reset_cache() def record(self, checkpoint: JournalDBCheckpoint) -> None: self._journal_storage.record(checkpoint) + self._changes_since_last_persist.record(checkpoint) def discard(self, checkpoint: JournalDBCheckpoint) -> None: self.logger.debug2('discard checkpoint %r', checkpoint) if self._journal_storage.has_checkpoint(checkpoint): self._journal_storage.discard(checkpoint) + self._changes_since_last_persist.discard(checkpoint) else: # if the checkpoint comes before this account started tracking, # then simply reset to the beginning self._journal_storage.reset() + self._changes_since_last_persist.reset() self._storage_cache.reset_cache() def commit(self, checkpoint: JournalDBCheckpoint) -> None: if self._journal_storage.has_checkpoint(checkpoint): self._journal_storage.commit(checkpoint) + self._changes_since_last_persist.commit(checkpoint) else: # if the checkpoint comes before this account started tracking, # then flatten all changes, without persisting self._journal_storage.flatten() + self._changes_since_last_persist.flatten() def make_storage_root(self) -> None: """ @@ -271,3 +288,14 @@ def persist(self, db: BaseDB) -> None: self._validate_flushed() if self._storage_lookup.has_changed_root: self._storage_lookup.commit_to(db) + + self._changes_since_last_persist.clear() + + def diff(self) -> DBDiff: + """ + Returns all the changes that would be saved if persist() were called. + + Note: Calling make_storage_root() wipes away changes, after it is called this method will + return an empty diff. + """ + return self._changes_since_last_persist.diff() diff --git a/eth/vm/base.py b/eth/vm/base.py index a932e0d0ee..f47f6a5c19 100644 --- a/eth/vm/base.py +++ b/eth/vm/base.py @@ -675,9 +675,17 @@ def finalize_block(self, block: BaseBlock) -> BaseBlock: # We need to call `persist` here since the state db batches # all writes until we tell it to write to the underlying db - self.state.persist() + # self.state.persist() - return block.copy(header=block.header.copy(state_root=self.state.state_root)) + # TODO: only do this if we're in turbo mode + # TODO: will we always know the hash here? + block_diff = self.state.persist_returning_block_diff() + + result = block.copy(header=block.header.copy(state_root=self.state.state_root)) + + basedb = self.chaindb.db + block_diff.write_to(basedb, result.hash) + return result def pack_block(self, block: BaseBlock, *args: Any, **kwargs: Any) -> BaseBlock: """ diff --git a/eth/vm/state.py b/eth/vm/state.py index d691bc2c54..c1c61ce326 100644 --- a/eth/vm/state.py +++ b/eth/vm/state.py @@ -26,6 +26,7 @@ from eth.db.account import ( BaseAccountDB, ) +from eth.db.block_diff import BlockDiff from eth.db.backends.base import ( BaseAtomicDB, ) @@ -253,6 +254,12 @@ def commit(self, snapshot: Tuple[Hash32, UUID]) -> None: def persist(self) -> None: self._account_db.persist() + def persist_returning_block_diff(self) -> BlockDiff: + """ + Persists all changes and also saves a record of them to the database. + """ + return self._account_db.persist_returning_block_diff() + # # Access self.prev_hashes (Read-only) # diff --git a/setup.py b/setup.py index 17dcbf8c8e..468df65226 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ "pytest-cov==2.5.1", "pytest-watch>=4.1.0,<5", "pytest-xdist==1.18.1", + "vyper==0.1.0b11", ], 'lint': [ "flake8==3.5.0", diff --git a/tests/core/chain-object/test_turbo_chain.py b/tests/core/chain-object/test_turbo_chain.py new file mode 100644 index 0000000000..6194f3c3c2 --- /dev/null +++ b/tests/core/chain-object/test_turbo_chain.py @@ -0,0 +1,100 @@ +""" +some tests that chain correctly manipulates the turbo database +""" +import pytest + +from eth_utils.toolz import ( + assoc, +) + +from eth.db.block_diff import BlockDiff +from eth.tools._utils.vyper import ( + compile_vyper_lll, +) + +from tests.core.helpers import ( + new_transaction, +) + + +CONTRACT_ADDRESS = b'\x10' * 20 + + +@pytest.fixture +def genesis_state(base_genesis_state): + """ + A little bit of magic, this overrides the genesis_state fixture which was defined elsewhere so + chain_without_block_validation uses the genesis state specified here. + """ + + # 1. when called this contract makes a simple change to the state + code = ['SSTORE', 0, 42] + bytecode = compile_vyper_lll(code)[0] + + # 2. put that code somewhere useful + return assoc( + base_genesis_state, + CONTRACT_ADDRESS, + { + 'balance': 0, + 'nonce': 0, + 'code': bytecode, + 'storage': {}, + } + ) + + +@pytest.fixture +def chain(chain_without_block_validation): + # make things a little less verbose + return chain_without_block_validation + + +def test_import_block_saves_block_diff(chain, funded_address, funded_address_private_key): + tx = new_transaction( + chain.get_vm(), + funded_address, + CONTRACT_ADDRESS, + data=b'', + private_key=funded_address_private_key, + ) + + new_block, _, _ = chain.build_block_with_transactions([tx]) + imported_block, _, _ = chain.import_block(new_block) + + imported_header = imported_block.header + imported_block_hash = imported_header.hash + + # sanity check, did the transaction go through? + assert len(imported_block.transactions) == 1 + state = chain.get_vm(imported_header).state + assert state.get_storage(CONTRACT_ADDRESS, 0) == 42 + + # the actual test, did we write out all the changes which happened? + base_db = chain.chaindb.db + diff = BlockDiff.from_db(base_db, imported_block_hash) + assert len(diff.get_changed_accounts()) == 3 + assert CONTRACT_ADDRESS in diff.get_changed_accounts() + assert imported_header.coinbase in diff.get_changed_accounts() + assert funded_address in diff.get_changed_accounts() + + assert diff.get_changed_slots(CONTRACT_ADDRESS) == {0} + assert diff.get_slot_change(CONTRACT_ADDRESS, 0) == (0, 42) + + assert diff.get_changed_slots(funded_address) == set() + assert diff.get_changed_slots(imported_header.coinbase) == set() + + # do some spot checks to make sure different fields were saved + + assert diff.get_decoded_account(imported_header.coinbase, new=False) is None + new_coinbase_balance = diff.get_decoded_account(imported_header.coinbase, new=True).balance + assert new_coinbase_balance > 0 + + old_sender_balance = diff.get_decoded_account(funded_address, new=False).balance + new_sender_balance = diff.get_decoded_account(funded_address, new=True).balance + assert old_sender_balance > new_sender_balance + + old_contract_nonce = diff.get_decoded_account(CONTRACT_ADDRESS, new=False).nonce + new_contract_nonce = diff.get_decoded_account(CONTRACT_ADDRESS, new=True).nonce + assert old_contract_nonce == 0 + assert new_contract_nonce == 0 diff --git a/tests/core/vm/test_block_diffs.py b/tests/core/vm/test_block_diffs.py new file mode 100644 index 0000000000..9610e4a7ca --- /dev/null +++ b/tests/core/vm/test_block_diffs.py @@ -0,0 +1,183 @@ +import pytest + +from eth_utils import int_to_big_endian + +from eth_hash.auto import keccak + +from eth.constants import BLANK_ROOT_HASH +from eth.db.atomic import AtomicDB +from eth.db.block_diff import BlockDiff +from eth.db.account import AccountDB +from eth.db.storage import StorageLookup + +ACCOUNT = b'\xaa' * 20 +BLOCK_HASH = keccak(b'one') + +""" +TODO: Some tests remain to be written: +- Test that this behavior is trigger during block import (if Turbo-mode is enabled) +- Test that this works even under calls to things like commit() and snapshot() +- Test that these diffs can be applied to something and the correct resulting state obtained +""" + + +@pytest.fixture +def base_db(): + return AtomicDB() + + +@pytest.fixture +def account_db(base_db): + return AccountDB(base_db) + + +# Some basic tests that BlockDiff works as expected and can round-trip data to the database + + +def test_no_such_diff_raises_key_error(base_db): + with pytest.raises(KeyError): + BlockDiff.from_db(base_db, BLOCK_HASH) + + +def test_can_persist_empty_block_diff(base_db): + orig = BlockDiff() + orig.write_to(base_db, BLOCK_HASH) + + block_diff = BlockDiff.from_db(base_db, BLOCK_HASH) + assert len(block_diff.get_changed_accounts()) == 0 + + +def test_can_persist_changed_account(base_db): + orig = BlockDiff() + orig.set_account_changed(ACCOUNT, b'old', b'new') # TODO: more realistic data + orig.write_to(base_db, BLOCK_HASH) + + block_diff = BlockDiff.from_db(base_db, BLOCK_HASH) + assert block_diff.get_changed_accounts() == {ACCOUNT} + assert block_diff.get_account(ACCOUNT, new=True) == b'new' + assert block_diff.get_account(ACCOUNT, new=False) == b'old' + + +# Some tests that AccountDB saves a block diff when persist()ing + + +def save_block_diff(account_db, block_hash): + diff = account_db.persist_returning_block_diff() + diff.write_to(account_db._raw_store_db, block_hash) + + +def test_account_diffs(account_db): + account_db.set_nonce(ACCOUNT, 10) + save_block_diff(account_db, BLOCK_HASH) + + diff = BlockDiff.from_db(account_db._raw_store_db, BLOCK_HASH) + assert diff.get_changed_accounts() == {ACCOUNT} + new_account = diff.get_decoded_account(ACCOUNT, new=True) + assert new_account.nonce == 10 + + assert diff.get_decoded_account(ACCOUNT, new=False) is None + + +def test_persists_storage_changes(account_db): + account_db.set_storage(ACCOUNT, 1, 10) + save_block_diff(account_db, BLOCK_HASH) + + diff = BlockDiff.from_db(account_db._raw_store_db, BLOCK_HASH) + assert diff.get_changed_accounts() == {ACCOUNT} + + assert diff.get_changed_slots(ACCOUNT) == {1} + assert diff.get_slot_change(ACCOUNT, 1) == (0, 10) + + +def test_persists_state_root(account_db): + """ + When the storage items change the account's storage root also changes and that change also + needs to be persisted. + """ + + # First, compute the expected new storage root + db = AtomicDB() + example_lookup = StorageLookup(db, BLANK_ROOT_HASH, ACCOUNT) + key = int_to_big_endian(1) + example_lookup[key] = int_to_big_endian(10) + expected_root = example_lookup.get_changed_root() + + # Next, make the same change to out storage + account_db.set_storage(ACCOUNT, 1, 10) + save_block_diff(account_db, BLOCK_HASH) + + # The new state root should have been included as part of the diff. + + diff = BlockDiff.from_db(account_db._raw_store_db, BLOCK_HASH) + assert diff.get_changed_accounts() == {ACCOUNT} + new_account = diff.get_decoded_account(ACCOUNT, new=True) + assert new_account.storage_root == expected_root + + +def test_two_storage_changes(account_db): + account_db.set_storage(ACCOUNT, 1, 10) + account_db.persist() + + account_db.set_storage(ACCOUNT, 1, 20) + save_block_diff(account_db, BLOCK_HASH) + + diff = BlockDiff.from_db(account_db._raw_store_db, BLOCK_HASH) + assert diff.get_changed_accounts() == {ACCOUNT} + + assert diff.get_changed_slots(ACCOUNT) == {1} + assert diff.get_slot_change(ACCOUNT, 1) == (10, 20) + + +def test_account_and_storage_change(account_db): + account_db.set_balance(ACCOUNT, 100) + account_db.set_storage(ACCOUNT, 1, 10) + + save_block_diff(account_db, BLOCK_HASH) + + diff = BlockDiff.from_db(account_db._raw_store_db, BLOCK_HASH) + assert diff.get_changed_accounts() == {ACCOUNT} + + old_account = diff.get_decoded_account(ACCOUNT, new=False) + assert old_account is None + + new_account = diff.get_decoded_account(ACCOUNT, new=True) + assert new_account.storage_root != BLANK_ROOT_HASH + assert new_account.balance == 100 + + assert diff.get_changed_slots(ACCOUNT) == {1} + assert diff.get_slot_change(ACCOUNT, 1) == (0, 10) + + +def test_delete_account(account_db): + account_db.set_balance(ACCOUNT, 100) + account_db.persist() + + account_db.delete_account(ACCOUNT) + save_block_diff(account_db, BLOCK_HASH) + + diff = BlockDiff.from_db(account_db._raw_store_db, BLOCK_HASH) + assert diff.get_changed_accounts() == {ACCOUNT} + old_account = diff.get_decoded_account(ACCOUNT, new=False) + new_account = diff.get_decoded_account(ACCOUNT, new=True) + + assert old_account.balance == 100 + assert new_account is None + + +def test_delete_storage(account_db): + """ + This is only called before a CREATE message is processed, and CREATE messages are not + allowed to overwrite non-empty storage tries (search for "collisions" in EIP 1014), so + this operation *should* always be a no-op. Here's a quick check to ensure block diff + handles it gracefully. + """ + + account_db.set_balance(ACCOUNT, 10) + account_db.delete_storage(ACCOUNT) + save_block_diff(account_db, BLOCK_HASH) + + diff = BlockDiff.from_db(account_db._raw_store_db, BLOCK_HASH) + assert diff.get_changed_slots(ACCOUNT) == set() + + new_account = diff.get_decoded_account(ACCOUNT, new=True) + assert new_account.balance == 10